diff --git a/key.py b/key.py new file mode 100644 index 0000000000000000000000000000000000000000..de89840834c0e7e52b5144dccf6a2d518de977e4 --- /dev/null +++ b/key.py @@ -0,0 +1,32 @@ +# management/generate_vapid_keys.py +# -*- coding: utf-8 -*- +import base64 +import os +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.backends import default_backend + +# تابعی برای تولید کلیدهای VAPID +def generate_vapid_keys(): + """ + این تابع یک جفت کلید عمومی و خصوصی VAPID تولید می‌کند. + """ + private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) + public_key = private_key.public_key() + + # برای استفاده در فرانت‌اند + public_key_bytes = public_key.public_numbers().x.to_bytes(32, 'big') + public_key.public_numbers().y.to_bytes(32, 'big') + public_key_b64 = base64.urlsafe_b64encode(public_key_bytes).decode('utf-8').rstrip('=') + + # برای استفاده در بک‌اند + private_key_bytes = private_key.private_numbers().private_value.to_bytes(32, 'big') + private_key_b64 = base64.urlsafe_b64encode(private_key_bytes).decode('utf-8').rstrip('=') + + print("VAPID Public Key:", public_key_b64) + print("VAPID Private Key:", private_key_b64) + +if __name__ == '__main__': + generate_vapid_keys() + + diff --git a/manage.py b/manage.py new file mode 100644 index 0000000000000000000000000000000000000000..24f05e825a5ccb2f17388273d07a0a36d9931d3d --- /dev/null +++ b/manage.py @@ -0,0 +1,20 @@ +#!/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', 'my_panel_project.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/management/__init__.py b/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/management/__pycache__/__init__.cpython-312.pyc b/management/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4d9d78579e26b4902c1a20d1979fd506a072008 Binary files /dev/null and b/management/__pycache__/__init__.cpython-312.pyc differ diff --git a/management/__pycache__/admin.cpython-312.pyc b/management/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30ac7512904fe6c0eccda461b7ee2a05d3fcf648 Binary files /dev/null and b/management/__pycache__/admin.cpython-312.pyc differ diff --git a/management/__pycache__/apps.cpython-312.pyc b/management/__pycache__/apps.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b7d10ce10207918c71160416b63a3eb81726baec Binary files /dev/null and b/management/__pycache__/apps.cpython-312.pyc differ diff --git a/management/__pycache__/backends.cpython-312.pyc b/management/__pycache__/backends.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bcf8604882b7e7250a4e16d86fa35a00333c2b73 Binary files /dev/null and b/management/__pycache__/backends.cpython-312.pyc differ diff --git a/management/__pycache__/forms.cpython-312.pyc b/management/__pycache__/forms.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e836df935d998bf7e64fa21b1eaf088b8080301 Binary files /dev/null and b/management/__pycache__/forms.cpython-312.pyc differ diff --git a/management/__pycache__/marzban_api.cpython-312.pyc b/management/__pycache__/marzban_api.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..526e124b737c34f60c546a72b0b6642916432e51 Binary files /dev/null and b/management/__pycache__/marzban_api.cpython-312.pyc differ diff --git a/management/__pycache__/marzban_client.cpython-312.pyc b/management/__pycache__/marzban_client.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2076b0aeceb8a4d394e02362fff7a616b842319 Binary files /dev/null and b/management/__pycache__/marzban_client.cpython-312.pyc differ diff --git a/management/__pycache__/models.cpython-312.pyc b/management/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..709eb1c5b3f0c9bc668d1420dd0f6f5938322249 Binary files /dev/null and b/management/__pycache__/models.cpython-312.pyc differ diff --git a/management/__pycache__/serializers.cpython-312.pyc b/management/__pycache__/serializers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b074e4fb86ce4f81bb37aaa7b734f0e409ca4a06 Binary files /dev/null and b/management/__pycache__/serializers.cpython-312.pyc differ diff --git a/management/__pycache__/tasks.cpython-312.pyc b/management/__pycache__/tasks.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6c8611ff00a244dddd83c5411a817950e5d33a5 Binary files /dev/null and b/management/__pycache__/tasks.cpython-312.pyc differ diff --git a/management/__pycache__/urls.cpython-312.pyc b/management/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5af555b9ef79cea0c2bb896ec771a625a0a4ada1 Binary files /dev/null and b/management/__pycache__/urls.cpython-312.pyc differ diff --git a/management/__pycache__/utils.cpython-312.pyc b/management/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2864c3b71335ceba0fd15c557c273c4fff269f09 Binary files /dev/null and b/management/__pycache__/utils.cpython-312.pyc differ diff --git a/management/__pycache__/views.cpython-312.pyc b/management/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e73eacdc987b610ff39be5f7df9addd9b821ef97 Binary files /dev/null and b/management/__pycache__/views.cpython-312.pyc differ diff --git a/management/admin.py b/management/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..fc6c269352653517cc032df62b715e7a814fdd2a --- /dev/null +++ b/management/admin.py @@ -0,0 +1,130 @@ +# management/admin.py +# -*- coding: utf-8 -*- +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from .models import ( + CustomUser, + AdminLevel2, + Panel, + License, + EndUser, + Plan, + Subscription, + Payment, + DiscountCode, + TelegramUser, + SecurityToken, + PushSubscription, + PaymentMethod, + PaymentSetting, + PaymentDetail +) + +# تنظیمات ادمین برای مدل CustomUser +@admin.register(CustomUser) +class CustomUserAdmin(BaseUserAdmin): + list_display = ('username', 'email', 'role', 'is_staff') + list_filter = ('role', 'is_staff', 'is_superuser', 'is_active') + search_fields = ('username', 'email') + ordering = ('username',) + fieldsets = BaseUserAdmin.fieldsets + ( + ('اطلاعات نقش', {'fields': ('role', 'marzban_admin_id')}), + ) + +# تنظیمات ادمین برای مدل AdminLevel2 +@admin.register(AdminLevel2) +class AdminLevel2Admin(admin.ModelAdmin): + # FIXED: 'marzban_api_token' does not exist in the model + list_display = ('user', 'telegram_chat_id', 'license_expiry_date') + search_fields = ('user__username',) + +# تنظیمات ادمین برای مدل Panel +@admin.register(Panel) +class PanelAdmin(admin.ModelAdmin): + # FIXED: 'url' and 'api_token' do not exist. Used correct fields. + list_display = ('name', 'owner', 'marzban_host') + search_fields = ('name', 'owner__username') + +# تنظیمات ادمین برای مدل License +@admin.register(License) +class LicenseAdmin(admin.ModelAdmin): + # FIXED: Used correct field names from the model ('key', 'owner', etc.) + list_display = ('key', 'owner', 'is_active', 'created_at', 'expiry_date') + list_filter = ('is_active',) + search_fields = ('key', 'owner__username') + +# تنظیمات ادمین برای مدل EndUser +@admin.register(EndUser) +class EndUserAdmin(admin.ModelAdmin): + list_display = ('username', 'panel', 'marzban_user_id') + search_fields = ('username', 'marzban_user_id') + list_filter = ('panel',) + +# تنظیمات ادمین برای مدل Plan +@admin.register(Plan) +class PlanAdmin(admin.ModelAdmin): + list_display = ('name', 'panel', 'price', 'duration_days', 'data_limit_gb', 'is_active') + list_filter = ('is_active', 'panel') + search_fields = ('name',) + +# تنظیمات ادمین برای مدل Subscription +@admin.register(Subscription) +class SubscriptionAdmin(admin.ModelAdmin): + list_display = ('end_user', 'plan', 'status', 'start_date', 'end_date', 'remaining_data_gb') + # FIXED: 'is_trial' does not exist in the model + list_filter = ('status', 'plan', 'panel') + search_fields = ('end_user__username',) + date_hierarchy = 'start_date' + +# تنظیمات ادمین برای مدل Payment +@admin.register(Payment) +class PaymentAdmin(admin.ModelAdmin): + list_display = ('subscription', 'admin', 'amount', 'status', 'created_at') + # IMPROVED: Filtering on image/text fields is not useful. Using status instead. + list_filter = ('status', 'admin') + search_fields = ('subscription__end_user__username',) + date_hierarchy = 'created_at' + +# تنظیمات ادمین برای مدل DiscountCode +@admin.register(DiscountCode) +class DiscountCodeAdmin(admin.ModelAdmin): + list_display = ('code', 'admin', 'discount_percentage', 'is_active') + list_filter = ('is_active',) + search_fields = ('code', 'admin__username',) + +# تنظیمات ادمین برای مدل TelegramUser +@admin.register(TelegramUser) +class TelegramUserAdmin(admin.ModelAdmin): + list_display = ('username', 'chat_id', 'admin_id') + search_fields = ('username', 'chat_id') + +# تنظیمات ادمین برای مدل SecurityToken +@admin.register(SecurityToken) +class SecurityTokenAdmin(admin.ModelAdmin): + list_display = ('admin_id', 'token', 'expiration_date') + search_fields = ('admin_id', 'token') + +# تنظیمات ادمین برای مدل PushSubscription +@admin.register(PushSubscription) +class PushSubscriptionAdmin(admin.ModelAdmin): + list_display = ('user', 'created_at') + search_fields = ('user__username',) + +# تنظیمات ادمین برای مدل PaymentMethod +@admin.register(PaymentMethod) +class PaymentMethodAdmin(admin.ModelAdmin): + list_display = ('name', 'is_active', 'can_be_managed_by_level3') + list_filter = ('is_active', 'can_be_managed_by_level3') + +# تنظیمات ادمین برای مدل PaymentSetting +@admin.register(PaymentSetting) +class PaymentSettingAdmin(admin.ModelAdmin): + list_display = ('admin_level_3', 'payment_method', 'is_active') + list_filter = ('is_active', 'payment_method') + search_fields = ('admin_level_3__user__username',) + +# تنظیمات ادمین برای مدل PaymentDetail +@admin.register(PaymentDetail) +class PaymentDetailAdmin(admin.ModelAdmin): + list_display = ('admin_level_3', 'card_number', 'wallet_address') + search_fields = ('admin_level_3__user__username',) diff --git a/management/backends.py b/management/backends.py new file mode 100644 index 0000000000000000000000000000000000000000..97ba5b716752e8a4f7415c79fd9e2d5bffabcfc2 --- /dev/null +++ b/management/backends.py @@ -0,0 +1,89 @@ +# management/backends.py +# -*- coding: utf-8 -*- + +import asyncio +import logging + +from django.contrib.auth.backends import BaseBackend +from django.contrib.auth import get_user_model +from marzban_api.marzban_client import Marzban +from marzban_api.exceptions import AuthenticationError, MarzbanAPIError + +from .models import MarzbanAdmin, MarzbanUser, Panel, Role + +CustomUser = get_user_model() +logger = logging.getLogger(__name__) + +class CustomMarzbanBackend(BaseBackend): + """ + بک‌اند احراز هویت که admin_id و اطلاعات ادمین را از دیتابیس مرزبان پیدا می‌کند. + """ + def authenticate(self, request, username=None, **kwargs): + if not username: + return None + + try: + # 1. پیدا کردن admin_id از دیتابیس مرزبان + marzban_user = MarzbanUser.objects.using('marzban_db').get(username=username) + admin_id = marzban_user.admin_id + + if not admin_id: + logger.warning(f"User {username} has no associated admin in Marzban DB.") + return None + + # 2. پیدا کردن اطلاعات ادمین از دیتابیس مرزبان + try: + admin_obj = MarzbanAdmin.objects.using('marzban_db').get(id=admin_id) + except MarzbanAdmin.DoesNotExist: + logger.error(f"Admin with ID {admin_id} not found in Marzban DB.") + return None + + admin_username = admin_obj.username + admin_password = admin_obj.password + + # 3. لاگین به API مرزبان با مشخصات ادمین و تایید وجود کاربر + async def authenticate_with_marzban(): + # در این مرحله، آدرس پنل باید از یک مکان مشخص (مثلاً مدل Panel) خوانده شود. + # برای سادگی، فعلا یک آدرس پیش‌فرض در نظر می‌گیریم. + panel_address = 'http://your_marzban_panel_address.com' + + try: + async with Marzban(admin_username, admin_password, panel_address) as marzban_client: + await marzban_client.login_admin() + await marzban_client.get_user_info(username) + + # 4. ایجاد یا دریافت کاربر در دیتابیس لوکال و اختصاص نقش + user, created = CustomUser.objects.get_or_create(username=username) + if created: + user_role, _ = Role.objects.get_or_create(name=Role.USER) + user.role = user_role + user.save() + return user + + except AuthenticationError as e: + logger.error(f"Marzban API authentication failed for admin {admin_username}: {e}") + return None + except MarzbanAPIError as e: + logger.error(f"User {username} not found in Marzban via admin {admin_username}: {e}") + return None + except Exception as e: + logger.error(f"An unknown error occurred during Marzban API authentication: {e}") + return None + + authenticated_user = asyncio.run(authenticate_with_marzban()) + if authenticated_user: + return authenticated_user + + except MarzbanUser.DoesNotExist: + logger.warning(f"User {username} not found in Marzban database.") + return None + except Exception as e: + logger.error(f"An unknown error occurred: {e}") + return None + + def get_user(self, user_id): + try: + return CustomUser.objects.get(pk=user_id) + except CustomUser.DoesNotExist: + return None + diff --git a/management/forms.py b/management/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..f97c0d9b487d5d9ef9a7a033ac7afa3a2eb13853 --- /dev/null +++ b/management/forms.py @@ -0,0 +1,13 @@ +# management/forms.py +# -*- coding: utf-8 -*- +from django import forms +from .models import DiscountCode + +class UserLoginForm(forms.Form): + username = forms.CharField(max_length=150, label="نام کاربری") + +class DiscountCodeForm(forms.ModelForm): + class Meta: + model = DiscountCode + fields = ['code', 'discount_type', 'value', 'expiration_date', 'is_active', 'min_new_subscriptions', 'max_uses', 'max_uses_per_user'] + diff --git a/management/manage.py b/management/manage.py new file mode 100644 index 0000000000000000000000000000000000000000..24f05e825a5ccb2f17388273d07a0a36d9931d3d --- /dev/null +++ b/management/manage.py @@ -0,0 +1,20 @@ +#!/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', 'my_panel_project.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/management/management/commands/backup_all_databases.py b/management/management/commands/backup_all_databases.py new file mode 100644 index 0000000000000000000000000000000000000000..880c0d4236a95a4940d4b197bc812251f53393b3 --- /dev/null +++ b/management/management/commands/backup_all_databases.py @@ -0,0 +1,100 @@ +# management/commands/backup_all_databases.py +# -*- coding: utf-8 -*- +import os +import datetime +import subprocess +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django.db import connections +from management.models import Panel +from management.tasks import send_backup_to_telegram + +class Command(BaseCommand): + help = 'Backs up all configured databases (Django and all Marzban panels) and sends them to Telegram.' + + def handle(self, *args, **options): + # Define the backup directory + backup_dir = os.path.join(settings.BASE_DIR, 'backups') + os.makedirs(backup_dir, exist_ok=True) + + # Create a timestamped directory for the current backup + timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + current_backup_dir = os.path.join(backup_dir, timestamp) + os.makedirs(current_backup_dir) + + # Get database configurations to backup + db_configs = { + 'default': connections['default'] + } + + # Dynamically add all Marzban panels to the backup list + try: + for panel in Panel.objects.all(): + db_key = f"marzban_panel_{panel.id}" + db_configs[db_key] = { + 'ENGINE': panel.db_engine, + 'NAME': panel.db_name, + 'USER': panel.db_user, + 'PASSWORD': panel.db_password, + 'HOST': panel.db_host, + 'PORT': panel.db_port, + 'TELEGRAM_BOT_TOKEN': panel.telegram_bot_token, + 'MARZBAN_PANEL_DOMAIN': panel.domain + } + except Exception as e: + self.stdout.write(self.style.ERROR( + f"Failed to retrieve Marzban panel information. Error: {e}" + )) + return + + for db_key, db_config in db_configs.items(): + if 'mysql' not in db_config.get('ENGINE', ''): + self.stdout.write(self.style.WARNING( + f"Skipping database '{db_key}' as it is not a MySQL database." + )) + continue + + output_file = os.path.join(current_backup_dir, f"{db_key}_{timestamp}.sql") + + mysqldump_cmd = [ + 'mysqldump', + f'--host={db_config.get("HOST", "localhost")}', + f'--port={db_config.get("PORT", 3306)}', + f'--user={db_config.get("USER", "")}', + f'--password={db_config.get("PASSWORD", "")}', + db_config['NAME'], + '--result-file', output_file + ] + + try: + self.stdout.write(f"Backing up database '{db_key}' to {output_file}...") + subprocess.run(mysqldump_cmd, check=True, text=True) + self.stdout.write(self.style.SUCCESS( + f"Successfully backed up database '{db_key}'." + )) + + # If it's a Marzban panel, send it to Telegram + if 'marzban' in db_key: + bot_token = db_config.get('TELEGRAM_BOT_TOKEN') + if bot_token and settings.TELEGRAM_ADMIN_CHAT_ID: + message = f"✅ Backup of Marzban panel at {db_config.get('MARZBAN_PANEL_DOMAIN', 'N/A')} is ready." + send_backup_to_telegram.delay( + settings.TELEGRAM_ADMIN_CHAT_ID, + message, + output_file, + bot_token + ) + self.stdout.write(self.style.SUCCESS( + f"Scheduled sending backup for '{db_key}' to Telegram." + )) + except FileNotFoundError: + raise CommandError( + "mysqldump command not found. Please ensure it is installed and in your system's PATH." + ) + except subprocess.CalledProcessError as e: + raise CommandError( + f"Failed to backup database '{db_key}'. Error: {e.stderr}" + ) + + self.stdout.write(self.style.SUCCESS('All databases backed up successfully.')) + diff --git a/management/management/commands/check_payments.py b/management/management/commands/check_payments.py new file mode 100644 index 0000000000000000000000000000000000000000..2c646a3db2e2c7df3efe0533d79dd15c7a4f0db9 --- /dev/null +++ b/management/management/commands/check_payments.py @@ -0,0 +1,42 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from datetime import timedelta +from management.models import Payment, Subscription + +class Command(BaseCommand): + help = 'Checks for pending payments older than 48 hours and deactivates subscriptions.' + + def handle(self, *args, **options): + # زمان فعلی منهای 48 ساعت + forty_eight_hours_ago = timezone.now() - timedelta(hours=48) + + # پیدا کردن پرداخت‌های در انتظار که بیش از 48 ساعت ازشون گذشته + overdue_payments = Payment.objects.filter( + status='pending', + created_at__lt=forty_eight_hours_ago + ) + + if not overdue_payments: + self.stdout.write(self.style.SUCCESS('No overdue pending payments found.')) + return + + self.stdout.write(self.style.WARNING(f'Found {overdue_payments.count()} overdue payments.')) + + # غیرفعال کردن اشتراک‌های مربوطه + for payment in overdue_payments: + try: + subscription = payment.subscription + if subscription and subscription.status == 'active': + subscription.status = 'inactive' + subscription.save() + payment.status = 'expired' + payment.save() + self.stdout.write(self.style.SUCCESS( + f'Successfully deactivated subscription for user: {subscription.user.username}' + )) + except Subscription.DoesNotExist: + self.stdout.write(self.style.ERROR( + f'Subscription not found for payment ID: {payment.id}' + )) + + self.stdout.write(self.style.SUCCESS('Finished checking payments.')) diff --git a/management/management/commands/restore_all_databases.py b/management/management/commands/restore_all_databases.py new file mode 100644 index 0000000000000000000000000000000000000000..6813a111be9656c4cf96ddcad7cdd3512f46aed9 --- /dev/null +++ b/management/management/commands/restore_all_databases.py @@ -0,0 +1,100 @@ +# management/commands/restore_all_databases.py +# -*- coding: utf-8 -*- +import os +import subprocess +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django.db import connections +from management.models import Panel + +class Command(BaseCommand): + help = 'Restores all databases from a specified timestamped backup directory.' + + def add_arguments(self, parser): + parser.add_argument( + 'timestamp', + type=str, + help='The timestamp of the backup directory to restore (e.g., 2023-10-27_10-30-00).', + ) + + def handle(self, *args, **options): + timestamp = options['timestamp'] + backup_dir = os.path.join(settings.BASE_DIR, 'backups', timestamp) + + if not os.path.isdir(backup_dir): + raise CommandError(f"Backup directory '{backup_dir}' not found.") + + self.stdout.write(self.style.WARNING( + f"WARNING: This will overwrite your current databases with data from '{timestamp}'. " + f"Proceed with caution. Type 'yes' to continue." + )) + confirm = input("> ") + if confirm.lower() != 'yes': + self.stdout.write("Restore operation cancelled.") + return + + # Get database configurations to restore + db_configs = { + 'default': connections['default'] + } + + try: + for panel in Panel.objects.all(): + db_key = f"marzban_panel_{panel.id}" + db_configs[db_key] = { + 'ENGINE': panel.db_engine, + 'NAME': panel.db_name, + 'USER': panel.db_user, + 'PASSWORD': panel.db_password, + 'HOST': panel.db_host, + 'PORT': panel.db_port, + } + except Exception as e: + self.stdout.write(self.style.ERROR( + f"Failed to retrieve Marzban panel information. Error: {e}" + )) + return + + + for db_key, db_config in db_configs.items(): + if 'mysql' not in db_config.get('ENGINE', ''): + self.stdout.write(self.style.WARNING( + f"Skipping restore for database '{db_key}' as it is not a MySQL database." + )) + continue + + sql_file = os.path.join(backup_dir, f"{db_key}_{timestamp}.sql") + + if not os.path.isfile(sql_file): + self.stdout.write(self.style.WARNING( + f"Skipping restore for database '{db_key}': backup file not found at {sql_file}." + )) + continue + + mysql_cmd = [ + 'mysql', + f'--host={db_config.get("HOST", "localhost")}', + f'--port={db_config.get("PORT", 3306)}', + f'--user={db_config.get("USER", "")}', + f'--password={db_config.get("PASSWORD", "")}', + db_config['NAME'] + ] + + try: + self.stdout.write(f"Restoring database '{db_key}' from {sql_file}...") + with open(sql_file, 'r') as f: + subprocess.run(mysql_cmd, stdin=f, check=True, text=True) + self.stdout.write(self.style.SUCCESS( + f"Successfully restored database '{db_key}'." + )) + except FileNotFoundError: + raise CommandError( + "mysql command not found. Please ensure it is installed and in your system's PATH." + ) + except subprocess.CalledProcessError as e: + raise CommandError( + f"Failed to restore database '{db_key}'. Error: {e.stderr}" + ) + + self.stdout.write(self.style.SUCCESS('All databases restored successfully.')) + diff --git a/management/management/commands/schedule_reminders.py b/management/management/commands/schedule_reminders.py new file mode 100644 index 0000000000000000000000000000000000000000..b3a5efa7bf9e66af71cf66299d81162d7f455ef1 --- /dev/null +++ b/management/management/commands/schedule_reminders.py @@ -0,0 +1,203 @@ +# management/management/commands/schedule_reminders.py + +# -*- coding: utf-8 -*- + +import os + +import uuid + +import datetime + +from django.core.management.base import BaseCommand + +from django.conf import settings + +from django.db import connections + +from management.models import Panel + +from management.tasks import send_telegram_notification + + + +# Dynamically import the unmanaged models + +# This will be used to access the Marzban database tables + +try: + +    from management.models import MarzbanUser, TelegramUser + +except ImportError: + +    # Handle the case where the models might not be fully defined yet + +    pass + + + +class Command(BaseCommand): + +    help = 'Schedules renewal and usage reminders for Marzban users.' + + + +    def handle(self, *args, **options): + +        # Retrieve the bot token from the environment variables or settings + +        # IMPORTANT: You need to define this in your settings or as an environment variable + +        telegram_bot_token = os.environ.get('TELEGRAM_BOT_TOKEN', 'YOUR_DEFAULT_BOT_TOKEN_HERE') + + + +        if not telegram_bot_token: + +            self.stdout.write(self.style.ERROR('Telegram bot token is not configured. Aborting.')) + +            return + + + +        self.stdout.write(self.style.SUCCESS('Starting reminder scheduling for all users...')) + + + +        # Get all panels from the main database + +        panels = Panel.objects.all() + + + +        for panel in panels: + +            self.stdout.write(f'Processing panel: {panel.name} (ID: {panel.id})') + +             + +            # Dynamically set up the database connection for the current panel + +            marzban_db_settings = { + +                'ENGINE': 'django.db.backends.mysql', + +                'NAME': panel.db_name, + +                'USER': panel.db_user, + +                'PASSWORD': panel.db_password, + +                'HOST': panel.db_host, + +                'PORT': panel.db_port, + +            } + +            settings.DATABASES['marzban_db'] = marzban_db_settings + + + +            try: + +                # Use the dynamic connection to query the MarzbanUser and TelegramUser models + +                with connections['marzban_db'].cursor() as cursor: + +                    # Get all users from the Marzban database + +                    users_to_check = MarzbanUser.objects.using('marzban_db').all() + + + +                    for user in users_to_check: + +                        # --- Here is the core logic for reminders --- + +                         + +                        # Find the Telegram chat_id for this user + +                        try: + +                            # We assume the TelegramUser model is now linked to the user's ID + +                            telegram_user = TelegramUser.objects.using('marzban_db').get(user_id=user.pk) + +                            chat_id = telegram_user.chat_id + +                        except TelegramUser.DoesNotExist: + +                            self.stdout.write(f'  No Telegram chat_id found for user {user.username}. Skipping.') + +                            continue + + + +                        # Check for expiration + +                        is_expiring = False + +                        if user.expire_date: + +                            time_left = user.expire_date - datetime.datetime.now(user.expire_date.tzinfo) + +                            if time_left < datetime.timedelta(days=3): # Expiring in less than 3 days + +                                is_expiring = True + +                                message = ( + +                                    f"⚠️ سلام {user.username}، سرویس شما در تاریخ {user.expire_date.strftime('%Y-%m-%d')} منقضی می‌شود.\n" + +                                    "لطفاً برای تمدید سرویس به پنل کاربری خود مراجعه کنید." + +                                ) + +                                send_telegram_notification.delay(chat_id, message, telegram_bot_token) + +                                self.stdout.write(f'  Scheduled expiration reminder for user {user.username}.') + + + +                        # Check for usage limit (e.g., 80% of total) + +                        is_over_usage_limit = False + +                        if user.data_limit and user.data_usage: + +                            usage_percentage = (user.data_usage / user.data_limit) * 100 + +                            if usage_percentage >= 80: + +                                is_over_usage_limit = True + +                                message = ( + +                                    f"⚠️ سلام {user.username}، حجم باقی‌مانده سرویس شما کم است.\n" + +                                    f"میزان مصرف: {usage_percentage:.2f}%\n" + +                                    "لطفاً برای خرید حجم جدید به پنل کاربری خود مراجعه کنید." + +                                ) + +                                # Avoid sending duplicate messages if both conditions are met + +                                if not is_expiring: + +                                    send_telegram_notification.delay(chat_id, message, telegram_bot_token) + +                                    self.stdout.write(f'  Scheduled usage reminder for user {user.username}.') + +                                 + +            except Exception as e: + +                self.stdout.write(self.style.ERROR(f'An error occurred while processing panel {panel.name}: {e}')) + + + +        self.stdout.write(self.style.SUCCESS('Reminder scheduling finished.')) + + diff --git a/management/marzban_client.py b/management/marzban_client.py new file mode 100644 index 0000000000000000000000000000000000000000..7c87b67183fe36a1bebc06e970d3521b03a59313 --- /dev/null +++ b/management/marzban_client.py @@ -0,0 +1,79 @@ +import aiohttp +import json +import asyncio + +# --- کد از فایل send_requests.py --- +async def send_request(endpoint, token, method, data=None): + panel_address = token["panel_address"] + token_type = token["token_type"] + access_token = token["access_token"] + request_address = f"{panel_address}/api/{endpoint}" + headers = { + "Content-Type": "application/json", + "Authorization": f"{token_type} {access_token}", + } + async with aiohttp.request( + method=method, + url=request_address, + headers=headers, + data=json.dumps(data) if data else None, + raise_for_status=True + ) as response: + result = await response.json() + return result + +# --- کد از فایل user.py --- +class User: + def __init__(self, username, **kwargs): + self.username = username + self.proxies = kwargs.get('proxies', {}) + self.inbounds = kwargs.get('inbounds', {}) + self.data_limit = kwargs.get('data_limit', 0) + self.data_limit_reset_strategy = kwargs.get('data_limit_reset_strategy', "no_reset") + self.status = kwargs.get('status', "") + self.expire = kwargs.get('expire', 0) + self.used_traffic = kwargs.get('used_traffic', 0) + self.lifetime_used_traffic = kwargs.get('lifetime_used_traffic', 0) + self.created_at = kwargs.get('created_at', "") + self.links = kwargs.get('links', []) + self.subscription_url = kwargs.get('subscription_url', "") + self.excluded_inbounds = kwargs.get('excluded_inbounds', {}) + self.full_name = kwargs.get('full_name', "") +class UserMethods: + async def get_all_users(self, token: dict, username=None, status=None): + endpoint = "users" + if username: + endpoint += f"?username={username}" + if status: + if "?" in endpoint: + endpoint += f"&status={status}" + else: + endpoint += f"?status={status}" + request = await send_request(endpoint, token, "get") + user_list = [User(**user) for user in request["users"]] + return user_list + +# --- کد از فایل admin.py --- +class Admin: + def __init__(self, username: str, password: str, panel_address: str): + self.username = username + self.password = password + self.panel_address = panel_address + + async def get_token(self): + try: + async with aiohttp.request( + "post", + url=f"{self.panel_address}/api/admin/token", + data={"username": self.username, "password": self.password}, + raise_for_status=True + ) as response: + result = await response.json() + result["panel_address"] = self.panel_address + return result + except aiohttp.exceptions.RequestException as ex: + print(f"Request Exception: {ex}") + return None + except json.JSONDecodeError as ex: + print(f"JSON Decode Error: {ex}") + return None diff --git a/management/migrations/0001_initial.py b/management/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..ee180a2ec9fbf6f241c7a170d0308961e6f1f5fe --- /dev/null +++ b/management/migrations/0001_initial.py @@ -0,0 +1,241 @@ +# Generated by Django 5.2.5 on 2025-08-15 09:56 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='SecurityToken', + fields=[ + ('admin_id', models.CharField(max_length=255, primary_key=True, serialize=False)), + ('token', models.CharField(default=uuid.uuid4, max_length=255, unique=True)), + ('expiration_date', models.DateTimeField()), + ], + options={ + 'db_table': 'telegram_tokens', + 'managed': False, + }, + ), + migrations.CreateModel( + name='TelegramUser', + fields=[ + ('admin_id', models.CharField(max_length=255, primary_key=True, serialize=False, unique=True)), + ('chat_id', models.CharField(max_length=255, unique=True)), + ('username', models.CharField(max_length=255)), + ], + options={ + 'db_table': 'telegram_users', + 'managed': False, + }, + ), + migrations.CreateModel( + name='MarzbanUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('username', models.CharField(max_length=255, unique=True)), + ], + ), + migrations.CreateModel( + name='PaymentMethod', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(choices=[('gateway', 'Bank Gateway'), ('crypto', 'Cryptocurrency'), ('manual', 'Manual (Card-to-Card)')], max_length=50, unique=True, verbose_name='Payment Method Name')), + ('is_active', models.BooleanField(default=True, verbose_name='Is Active?')), + ('can_be_managed_by_level3', models.BooleanField(default=False, verbose_name='Can be managed by Admin Level 3?')), + ], + options={ + 'verbose_name': 'Payment Method', + 'verbose_name_plural': 'Payment Methods', + }, + ), + migrations.CreateModel( + name='CustomUser', + 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')), + ('role', models.CharField(choices=[('SuperAdmin', 'Super Admin'), ('PanelOwner', 'Panel Owner'), ('AdminLevel2', 'Admin Level 2'), ('AdminLevel3', 'Admin Level 3'), ('EndUser', 'End User')], default='EndUser', max_length=20)), + ('marzban_admin_id', models.UUIDField(blank=True, null=True)), + ('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': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='AdminLevel2', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('telegram_chat_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='تلگرام چت آیدی')), + ('license_expiry_date', models.DateField(blank=True, null=True, verbose_name='تاریخ انقضای لایسنس')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='admin_level_2', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='AdminLevel3', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('telegram_chat_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='تلگرام چت آیدی')), + ('parent_admin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='child_admins', to='management.adminlevel2', verbose_name='ادمین والد (سطح ۲)')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='admin_level_3', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='DiscountCode', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50, unique=True)), + ('discount_percentage', models.DecimalField(decimal_places=2, max_digits=5)), + ('is_active', models.BooleanField(default=True)), + ('admin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='License', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='کلید لایسنس')), + ('is_active', models.BooleanField(default=True, verbose_name='وضعیت فعال')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')), + ('expiry_date', models.DateField(verbose_name='تاریخ انقضا')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='صاحب لایسنس')), + ], + ), + migrations.CreateModel( + name='Panel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='نام پنل')), + ('marzban_host', models.CharField(max_length=255, verbose_name='آدرس هاست Marzban')), + ('marzban_username', models.CharField(max_length=255, verbose_name='نام کاربری Marzban')), + ('marzban_password', models.CharField(max_length=255, verbose_name='رمز عبور Marzban')), + ('telegram_bot_token', models.CharField(blank=True, max_length=255, null=True)), + ('license', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='linked_panel', to='management.license')), + ('owner', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='owned_panel', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='MarzbanAdmin', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('username', models.CharField(max_length=255, unique=True)), + ('password', models.CharField(max_length=255)), + ('permission', models.CharField(max_length=50)), + ('panel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='marzban_admins', to='management.panel')), + ], + ), + migrations.CreateModel( + name='EndUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('username', models.CharField(max_length=255, unique=True)), + ('marzban_user_id', models.CharField(blank=True, max_length=255, null=True, unique=True)), + ('telegram_chat_id', models.CharField(blank=True, max_length=255, null=True)), + ('panel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='management.panel')), + ], + ), + migrations.CreateModel( + name='PaymentDetail', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('card_number', models.CharField(blank=True, max_length=50, null=True)), + ('card_holder_name', models.CharField(blank=True, max_length=255, null=True)), + ('wallet_address', models.CharField(blank=True, max_length=255, null=True)), + ('admin_level_3', models.OneToOneField(limit_choices_to={'role': 'AdminLevel3'}, on_delete=django.db.models.deletion.CASCADE, related_name='payment_details', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Payment Detail', + 'verbose_name_plural': 'Payment Details', + }, + ), + migrations.CreateModel( + name='Plan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('duration_days', models.IntegerField()), + ('data_limit_gb', models.FloatField()), + ('is_active', models.BooleanField(default=True)), + ('panel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plans', to='management.panel')), + ], + ), + migrations.CreateModel( + name='PushSubscription', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subscription_info', models.JSONField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='push_subscriptions', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Subscription', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('active', 'فعال'), ('expired', 'منقضی شده'), ('pending', 'در انتظار پرداخت'), ('deactivated', 'غیرفعال')], default='pending', max_length=20)), + ('start_date', models.DateField(blank=True, null=True)), + ('end_date', models.DateField(blank=True, null=True)), + ('remaining_data_gb', models.FloatField(blank=True, null=True)), + ('end_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='management.enduser')), + ('panel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='management.panel')), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='management.plan')), + ], + ), + migrations.CreateModel( + name='Payment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='مبلغ')), + ('status', models.CharField(choices=[('pending', 'در انتظار تأیید'), ('approved', 'تأیید شده'), ('rejected', 'رد شده')], default='pending', max_length=20, verbose_name='وضعیت')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')), + ('payment_token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('receipt_image', models.ImageField(blank=True, null=True, upload_to='receipts/', verbose_name='تصویر رسید')), + ('receipt_text', models.TextField(blank=True, null=True, verbose_name='متن رسید')), + ('admin', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='handled_payments', to=settings.AUTH_USER_MODEL)), + ('subscription', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='management.subscription')), + ], + ), + migrations.CreateModel( + name='PaymentSetting', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_active', models.BooleanField(default=False, verbose_name='Is Active for this Admin?')), + ('admin_level_3', models.ForeignKey(limit_choices_to={'role': 'AdminLevel3'}, on_delete=django.db.models.deletion.CASCADE, related_name='payment_settings', to=settings.AUTH_USER_MODEL)), + ('payment_method', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='management.paymentmethod')), + ], + options={ + 'verbose_name': 'Payment Setting', + 'verbose_name_plural': 'Payment Settings', + 'unique_together': {('admin_level_3', 'payment_method')}, + }, + ), + ] diff --git a/management/migrations/__init__.py b/management/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/management/migrations/__pycache__/0001_initial.cpython-312.pyc b/management/migrations/__pycache__/0001_initial.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14b95d646d38beab232dc026bc71fbe60ea371d0 Binary files /dev/null and b/management/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/management/migrations/__pycache__/0002_remove_user_admin_remove_payment_user_and_more.cpython-312.pyc b/management/migrations/__pycache__/0002_remove_user_admin_remove_payment_user_and_more.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..092169e77bda7d21cb1bbe51e6503d5fd82425bd Binary files /dev/null and b/management/migrations/__pycache__/0002_remove_user_admin_remove_payment_user_and_more.cpython-312.pyc differ diff --git a/management/migrations/__pycache__/0003_notification_remove_payment_user_config_and_more.cpython-312.pyc b/management/migrations/__pycache__/0003_notification_remove_payment_user_config_and_more.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e749dfc8d049459e72c37a74eec7b4d20a1b6e2e Binary files /dev/null and b/management/migrations/__pycache__/0003_notification_remove_payment_user_config_and_more.cpython-312.pyc differ diff --git a/management/migrations/__pycache__/__init__.cpython-312.pyc b/management/migrations/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e9816f18de42e3d192d241af84dbc00defcec041 Binary files /dev/null and b/management/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/management/models.py b/management/models.py new file mode 100644 index 0000000000000000000000000000000000000000..f3e84b69b3fc9bf014d3afb33d70de8fbed4d482 --- /dev/null +++ b/management/models.py @@ -0,0 +1,261 @@ +# management/models.py +# -*- coding: utf-8 -*- +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.utils import timezone +import uuid +import datetime + +# --- Users & Authentication --- +class CustomUser(AbstractUser): + ROLE_CHOICES = [ + ('SuperAdmin', 'Super Admin'), + ('PanelOwner', 'Panel Owner'), + ('AdminLevel2', 'Admin Level 2'), + ('AdminLevel3', 'Admin Level 3'), + ('EndUser', 'End User'), + ] + role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='EndUser') + marzban_admin_id = models.UUIDField(null=True, blank=True) + + def is_super_admin(self): + return self.role == 'SuperAdmin' + + def is_admin_level_2(self): + return self.role in ('SuperAdmin', 'PanelOwner', 'AdminLevel2') + + def is_admin_level_3(self): + return self.role in ('SuperAdmin', 'PanelOwner', 'AdminLevel2', 'AdminLevel3') + + def __str__(self): + return self.username + +# --- Main Website Models --- +class AdminLevel2(models.Model): + user = models.OneToOneField( + CustomUser, on_delete=models.CASCADE, related_name='admin_level_2' + ) + telegram_chat_id = models.CharField( + max_length=255, blank=True, null=True, + verbose_name="تلگرام چت آیدی" + ) + license_expiry_date = models.DateField( + null=True, blank=True, verbose_name="تاریخ انقضای لایسنس" + ) + def __str__(self): + return f"Admin Level 2: {self.user.username}" + + +class AdminLevel3(models.Model): + user = models.OneToOneField( + CustomUser, on_delete=models.CASCADE, related_name='admin_level_3' + ) + parent_admin = models.ForeignKey( + AdminLevel2, + on_delete=models.CASCADE, + related_name='child_admins', + verbose_name="ادمین والد (سطح ۲)" + ) + telegram_chat_id = models.CharField( + max_length=255, blank=True, null=True, + verbose_name="تلگرام چت آیدی" + ) + def __str__(self): + return f"Admin Level 3: {self.user.username}" + +class Panel(models.Model): + owner = models.OneToOneField(CustomUser, on_delete=models.CASCADE, related_name='owned_panel') + name = models.CharField(max_length=255, verbose_name="نام پنل") + marzban_host = models.CharField(max_length=255, verbose_name="آدرس هاست Marzban") + marzban_username = models.CharField(max_length=255, verbose_name="نام کاربری Marzban") + marzban_password = models.CharField(max_length=255, verbose_name="رمز عبور Marzban") + telegram_bot_token = models.CharField(max_length=255, blank=True, null=True) + # ADDED: Link to the license + license = models.OneToOneField('License', on_delete=models.SET_NULL, null=True, blank=True, related_name='linked_panel') + + def __str__(self): + return self.name + +class License(models.Model): + key = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name="کلید لایسنس") + is_active = models.BooleanField(default=True, verbose_name="وضعیت فعال") + owner = models.ForeignKey(CustomUser, on_delete=models.CASCADE, verbose_name="صاحب لایسنس") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ ایجاد") + expiry_date = models.DateField(verbose_name="تاریخ انقضا") + + def __str__(self): + return str(self.key) + +# ADDED: MarzbanAdmin model (was missing) +class MarzbanAdmin(models.Model): + username = models.CharField(max_length=255, unique=True) + password = models.CharField(max_length=255) + permission = models.CharField(max_length=50) + panel = models.ForeignKey(Panel, on_delete=models.CASCADE, related_name='marzban_admins') + + def __str__(self): + return self.username + +# ADDED: MarzbanUser model (was missing) +class MarzbanUser(models.Model): + username = models.CharField(max_length=255, unique=True) + # Add other fields relevant to Marzban users + # ... + + def __str__(self): + return self.username + +class EndUser(models.Model): + username = models.CharField(max_length=255, unique=True) + panel = models.ForeignKey(Panel, on_delete=models.CASCADE) + # marzban_user_id can be nullable if the user is created first in our DB + marzban_user_id = models.CharField(max_length=255, unique=True, null=True, blank=True) + telegram_chat_id = models.CharField(max_length=255, blank=True, null=True) + + def __str__(self): + return self.username + +# ADDED: Plan model (was missing) +class Plan(models.Model): + name = models.CharField(max_length=100) + price = models.DecimalField(max_digits=10, decimal_places=2) + duration_days = models.IntegerField() + data_limit_gb = models.FloatField() + is_active = models.BooleanField(default=True) + panel = models.ForeignKey(Panel, on_delete=models.CASCADE, related_name='plans') + + def __str__(self): + return f"{self.name} ({self.panel.name})" + +# ADDED: Subscription model (was missing) +class Subscription(models.Model): + STATUS_CHOICES = [ + ('active', 'فعال'), + ('expired', 'منقضی شده'), + ('pending', 'در انتظار پرداخت'), + ('deactivated', 'غیرفعال'), + ] + end_user = models.ForeignKey(EndUser, on_delete=models.CASCADE, related_name='subscriptions') + plan = models.ForeignKey(Plan, on_delete=models.PROTECT) # Don't delete plan if subscription exists + panel = models.ForeignKey(Panel, on_delete=models.CASCADE, related_name='subscriptions') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') + start_date = models.DateField(null=True, blank=True) + end_date = models.DateField(null=True, blank=True) + remaining_data_gb = models.FloatField(null=True, blank=True) + + def __str__(self): + return f"Subscription for {self.end_user.username} on plan {self.plan.name}" + +# FIXED: Complete overhaul of the Payment model +class Payment(models.Model): + STATUS_CHOICES = [ + ('pending', 'در انتظار تأیید'), + ('approved', 'تأیید شده'), + ('rejected', 'رد شده'), + ] + subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE, related_name='payments') + admin = models.ForeignKey(CustomUser, on_delete=models.PROTECT, related_name='handled_payments') # The admin to approve + amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="مبلغ") + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="وضعیت") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ ایجاد") + + # Fields for receipt upload + payment_token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) + receipt_image = models.ImageField(upload_to='receipts/', blank=True, null=True, verbose_name="تصویر رسید") + receipt_text = models.TextField(blank=True, null=True, verbose_name="متن رسید") + + def __str__(self): + return f"Payment for {self.subscription.end_user.username} - {self.amount} - {self.status}" + + +class DiscountCode(models.Model): + code = models.CharField(max_length=50, unique=True) + admin = models.ForeignKey(CustomUser, on_delete=models.CASCADE) + discount_percentage = models.DecimalField(max_digits=5, decimal_places=2) + is_active = models.BooleanField(default=True) + def __str__(self): + return self.code + +# --- Telegram Integration --- +class TelegramUser(models.Model): + admin_id = models.CharField(max_length=255, unique=True, primary_key=True) + chat_id = models.CharField(max_length=255, unique=True) + username = models.CharField(max_length=255) + class Meta: + managed = False + db_table = 'telegram_users' + def __str__(self): + return self.username + +class SecurityToken(models.Model): + admin_id = models.CharField(max_length=255, primary_key=True) + token = models.CharField(max_length=255, unique=True, default=uuid.uuid4) + expiration_date = models.DateTimeField() + class Meta: + managed = False + db_table = 'telegram_tokens' + def __str__(self): + return f"Token for {self.admin_id}" + +# --- Push Notifications --- +class PushSubscription(models.Model): + user = models.ForeignKey( + CustomUser, on_delete=models.CASCADE, related_name='push_subscriptions' + ) + subscription_info = models.JSONField() + created_at = models.DateTimeField(auto_now_add=True) + +# --- Payment System Models --- +class PaymentMethod(models.Model): + METHOD_CHOICES = [ + ('gateway', 'Bank Gateway'), + ('crypto', 'Cryptocurrency'), + ('manual', 'Manual (Card-to-Card)'), + ] + name = models.CharField(max_length=50, choices=METHOD_CHOICES, unique=True, verbose_name="Payment Method Name") + is_active = models.BooleanField(default=True, verbose_name="Is Active?") + can_be_managed_by_level3 = models.BooleanField(default=False, verbose_name="Can be managed by Admin Level 3?") + + class Meta: + verbose_name = "Payment Method" + verbose_name_plural = "Payment Methods" + + def __str__(self): + return self.get_name_display() + +class PaymentSetting(models.Model): + admin_level_3 = models.ForeignKey( + CustomUser, + on_delete=models.CASCADE, + limit_choices_to={'role': 'AdminLevel3'}, + related_name='payment_settings' + ) + payment_method = models.ForeignKey(PaymentMethod, on_delete=models.CASCADE) + is_active = models.BooleanField(default=False, verbose_name="Is Active for this Admin?") + + class Meta: + unique_together = ('admin_level_3', 'payment_method') + verbose_name = "Payment Setting" + verbose_name_plural = "Payment Settings" + + def __str__(self): + return f"{self.admin_level_3.username}'s {self.payment_method.get_name_display()} Setting" + +class PaymentDetail(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + admin_level_3 = models.OneToOneField( + CustomUser, + on_delete=models.CASCADE, + limit_choices_to={'role': 'AdminLevel3'}, + related_name='payment_details' + ) + card_number = models.CharField(max_length=50, blank=True, null=True) + card_holder_name = models.CharField(max_length=255, blank=True, null=True) + wallet_address = models.CharField(max_length=255, blank=True, null=True) + + class Meta: + verbose_name = "Payment Detail" + verbose_name_plural = "Payment Details" + + def __str__(self): + return f"Payment details for {self.admin_level_3.username}" diff --git a/management/serializers.py b/management/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..606d71b4f137787cfe3717ac67cecf0ddea51d38 --- /dev/null +++ b/management/serializers.py @@ -0,0 +1,193 @@ +# management/serializers.py +# -*- coding: utf-8 -*- +from rest_framework import serializers +from django.utils import timezone +import datetime +from .models import ( + CustomUser, + Panel, + License, + PushSubscription, + MarzbanAdmin, + SecurityToken, + DiscountCode, + Plan, + Subscription, + Payment, + EndUser, + PaymentMethod, + PaymentSetting, + PaymentDetail, +) + +# --- Serializers for Django Rest Framework --- + +# ADDED: LicenseSerializer (was missing) +class LicenseSerializer(serializers.ModelSerializer): + """Serializer for the License model.""" + class Meta: + model = License + fields = ['key', 'is_active', 'owner', 'created_at', 'expiry_date'] + read_only_fields = ['key', 'created_at'] + +class CustomUserSerializer(serializers.ModelSerializer): + class Meta: + model = CustomUser + fields = ['id', 'username', 'role'] + +class PushSubscriptionSerializer(serializers.Serializer): + subscription_info = serializers.JSONField() + +class MarzbanAdminSerializer(serializers.ModelSerializer): + class Meta: + model = MarzbanAdmin + fields = ['id', 'username', 'password', 'permission'] + extra_kwargs = {'password': {'write_only': True}} + +class SecurityTokenSerializer(serializers.ModelSerializer): + class Meta: + model = SecurityToken + fields = ['admin_id', 'token', 'expiration_date'] + +class DiscountCodeSerializer(serializers.ModelSerializer): + class Meta: + model = DiscountCode + fields = ['id', 'code', 'discount_percentage', 'is_active'] + read_only_fields = ['id', 'admin'] + +class PanelSerializer(serializers.ModelSerializer): + license_key = serializers.CharField(write_only=True, required=True) + + class Meta: + model = Panel + fields = ['id', 'name', 'marzban_host', 'marzban_username', 'marzban_password', 'telegram_bot_token', 'license_key'] + read_only_fields = ['id', 'owner'] + + def validate_license_key(self, value): + owner = self.context['request'].user + try: + license_obj = License.objects.get(key=value, owner=owner, is_active=True) + if hasattr(license_obj, 'linked_panel') and license_obj.linked_panel is not None: + raise serializers.ValidationError('This license key has already been used.') + return license_obj + except License.DoesNotExist: + raise serializers.ValidationError('Invalid or inactive license key.') + + def create(self, validated_data): + license_obj = validated_data.pop('license_key') + panel = Panel.objects.create( + owner=self.context['request'].user, + license=license_obj, + **validated_data + ) + return panel + +class PlanSerializer(serializers.ModelSerializer): + class Meta: + model = Plan + fields = ['id', 'name', 'price', 'duration_days', 'data_limit_gb', 'is_active'] + read_only_fields = ['id'] + +class SubscriptionSerializer(serializers.ModelSerializer): + end_user = serializers.CharField(source='end_user.username', read_only=True) + plan = PlanSerializer(read_only=True) + + class Meta: + model = Subscription + fields = ['id', 'end_user', 'plan', 'status', 'start_date', 'end_date', 'remaining_data_gb'] + +class AdminLevel3SubscriptionSerializer(serializers.ModelSerializer): + end_user_username = serializers.CharField(write_only=True) + plan_id = serializers.IntegerField(write_only=True) + end_user = SubscriptionSerializer(source='end_user', read_only=True) # To show user details on response + plan = PlanSerializer(read_only=True) # To show plan details on response + + class Meta: + model = Subscription + fields = ['id', 'end_user_username', 'plan_id', 'status', 'start_date', 'end_date', 'remaining_data_gb', 'end_user', 'plan'] + read_only_fields = ['id', 'status', 'start_date', 'end_date', 'remaining_data_gb', 'end_user', 'plan'] + + def validate(self, data): + request_user = self.context['request'].user + try: + panel = request_user.owned_panel + except Panel.DoesNotExist: + raise serializers.ValidationError("Admin does not own a panel.") + + try: + end_user = EndUser.objects.get(username=data.get('end_user_username'), panel=panel) + data['end_user_instance'] = end_user + except EndUser.DoesNotExist: + raise serializers.ValidationError({"end_user_username": "User not found in your panel."}) + + try: + plan = Plan.objects.get(id=data.get('plan_id'), is_active=True, panel=panel) + data['plan_instance'] = plan + except Plan.DoesNotExist: + raise serializers.ValidationError({"plan_id": "Plan not found or is not active in your panel."}) + + data['panel_instance'] = panel + return data + + def create(self, validated_data): + end_user = validated_data.get('end_user_instance') + plan = validated_data.get('plan_instance') + panel = validated_data.get('panel_instance') + + start_date = timezone.now().date() + end_date = start_date + datetime.timedelta(days=plan.duration_days) + + subscription = Subscription.objects.create( + end_user=end_user, + plan=plan, + panel=panel, + status='active', # Assuming direct creation by admin activates it + start_date=start_date, + end_date=end_date, + remaining_data_gb=plan.data_limit_gb + ) + return subscription + +class PurchaseSubscriptionForUserSerializer(serializers.Serializer): + end_user_username = serializers.CharField(max_length=150) + plan_id = serializers.IntegerField() + amount = serializers.DecimalField(max_digits=10, decimal_places=2) + + def validate_plan_id(self, value): + if not Plan.objects.filter(id=value, is_active=True).exists(): + raise serializers.ValidationError("Plan not found or is not active.") + return value + +class ReceiptUploadSerializer(serializers.ModelSerializer): + payment_token = serializers.UUIDField(write_only=True) + + class Meta: + model = Payment + fields = ['receipt_image', 'receipt_text', 'payment_token'] + + def validate(self, data): + if not data.get('receipt_image') and not data.get('receipt_text'): + raise serializers.ValidationError("Either receipt image or text is required.") + return data + +# --- Payment System Serializers --- + +class PaymentMethodSerializer(serializers.ModelSerializer): + class Meta: + model = PaymentMethod + fields = ['id', 'name', 'is_active', 'can_be_managed_by_level3'] + read_only_fields = ['id'] + +class PaymentDetailSerializer(serializers.ModelSerializer): + class Meta: + model = PaymentDetail + fields = ['id', 'card_number', 'card_holder_name', 'wallet_address'] + read_only_fields = ['id'] + +class EndUserPaymentDetailSerializer(serializers.Serializer): + method_name = serializers.CharField(source='payment_method.get_name_display') + method_key = serializers.CharField(source='payment_method.name') + is_active = serializers.BooleanField(source='is_active') + card_number = serializers.CharField(source='admin_level_3.payment_details.card_number', read_only=True) + card_holder_name = serializers.CharField(source='admin_level_3.payment_details.card_holder_name', read_only=True) + wallet_address = serializers.CharField(source='admin_level_3.payment_details.wallet_address', read_only=True) diff --git a/management/static/css/styles.css b/management/static/css/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..d6e60313377e679b0787a4dd4b983cff334d9cd7 --- /dev/null +++ b/management/static/css/styles.css @@ -0,0 +1,22 @@ +/* management/static/css/styles.css */ +@font-face { + font-family: 'Vazirmatn'; + src: url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.0.3/fonts/web/eot/Vazirmatn-Regular.eot') format('eot'), + url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.0.3/fonts/web/woff2/Vazirmatn-Regular.woff2') format('woff2'), + url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.0.3/fonts/web/woff/Vazirmatn-Regular.woff') format('woff'), + url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.0.3/fonts/web/ttf/Vazirmatn-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +body { + font-family: 'Vazirmatn', sans-serif; +} + +/* Custom styles for the configs list */ +#configs-list li:hover { + background-color: #f1f5f9; + transition: background-color 0.2s ease-in-out; +} + diff --git a/management/static/js/main.js b/management/static/js/main.js new file mode 100644 index 0000000000000000000000000000000000000000..88979c30178bfed1aefa971d5cc92945fef69fc6 --- /dev/null +++ b/management/static/js/main.js @@ -0,0 +1,174 @@ +// management/static/js/main.js +// -*- coding: utf-8 -*- + +// VAPID_PUBLIC_KEY will be passed from the Django template. +// This is a placeholder; it will be filled with the correct value in the HTML file. +const VAPID_PUBLIC_KEY_PLACEHOLDER = "{{ vapid_public_key }}"; + +/** + * Converts a base64 string to a Uint8Array. + * @param {string} base64String + * @returns {Uint8Array} + */ +function urlBase64ToUint8Array(base64String) { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +} + +/** + * Registers the service worker for the PWA. + * @returns {Promise} + */ +async function registerServiceWorker() { + if ('serviceWorker' in navigator) { + try { + const registration = await navigator.serviceWorker.register('/service-worker.js'); + console.log('Service worker registered successfully.'); + return registration; + } catch (error) { + console.error('Service worker registration failed:', error); + return null; + } + } + console.error('Service workers are not supported in this browser.'); + return null; +} + +/** + * Subscribes the user to push notifications. + * @param {ServiceWorkerRegistration} registration + */ +async function subscribeUserToPush(registration) { + if (!('PushManager' in window)) { + console.error('Push notifications are not supported.'); + return; + } + + if (!VAPID_PUBLIC_KEY_PLACEHOLDER || VAPID_PUBLIC_KEY_PLACEHOLDER === "YOUR_VAPID_PUBLIC_KEY_HERE") { + console.error("VAPID public key not configured."); + return; + } + + try { + const subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY_PLACEHOLDER) + }); + + console.log('User is subscribed to push.'); + const response = await fetch('/api/push/subscribe/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + }, + body: JSON.stringify(subscription) + }); + + if (!response.ok) { + console.error('Failed to save push subscription on server.'); + } + + } catch (error) { + console.error('Push subscription failed:', error); + } +} + +/** + * Fetches and displays user dashboard data from the API. + */ +async function fetchDashboardData() { + try { + const response = await fetch('/api/dashboard/'); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const data = await response.json(); + document.getElementById('username-display').textContent = data.username; + document.getElementById('service-status').textContent = data.service_status; + document.getElementById('used-traffic').textContent = (data.used_traffic / (1024 ** 3)).toFixed(2) + ' GB'; + document.getElementById('total-traffic').textContent = (data.total_traffic / (1024 ** 3)).toFixed(2) + ' GB'; + + const configsList = document.getElementById('configs-list'); + configsList.innerHTML = ''; // Clear previous content + data.configs.forEach(config => { + const listItem = document.createElement('li'); + listItem.className = 'bg-gray-50 rounded-md p-3 border border-gray-200'; + listItem.innerHTML = ` +
${config.remark}
+
${config.link}
+ `; + configsList.appendChild(listItem); + }); + + } catch (error) { + console.error('Error fetching dashboard data:', error); + document.body.innerHTML = '

خطا در بارگذاری اطلاعات داشبورد.

'; + } +} + +/** + * Handles the generation of the Telegram security token. + */ +async function handleTelegramTokenGeneration() { + const generateTokenBtn = document.getElementById('generate-token-btn'); + const tokenDisplayArea = document.getElementById('token-display-area'); + const telegramTokenCode = document.getElementById('telegram-token'); + + if (!generateTokenBtn) return; + + generateTokenBtn.addEventListener('click', async () => { + const response = await fetch('/api/telegram/generate-token/', { + method: 'POST', + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }); + if (response.ok) { + const data = await response.json(); + telegramTokenCode.textContent = data.token; + tokenDisplayArea.classList.remove('hidden'); + } else { + // Using a custom modal or message box instead of alert() + console.error('Failed to generate token.'); + alert('خطا در تولید توکن.'); + } + }); +} + +document.addEventListener('DOMContentLoaded', async () => { + // Fetch and render dashboard data + await fetchDashboardData(); + + // PWA and Push Notification logic + const registration = await registerServiceWorker(); + if (registration) { + if (Notification.permission === 'granted') { + console.log('Push permission already granted.'); + } else if (Notification.permission === 'denied') { + console.log('Push permission denied by user.'); + } else { + // Prompt the user for permission + // This is a browser feature, so we don't need a custom modal + Notification.requestPermission().then(permission => { + if (permission === 'granted') { + subscribeUserToPush(registration); + } else { + console.log('User denied push notification permission.'); + } + }); + } + } + + // Telegram token generation + handleTelegramTokenGeneration(); +}); + diff --git a/management/static/js/service-worker.js b/management/static/js/service-worker.js new file mode 100644 index 0000000000000000000000000000000000000000..db94dcb1fe454efa3e0811447be618db3ba09bc7 --- /dev/null +++ b/management/static/js/service-worker.js @@ -0,0 +1,91 @@ +// management/templates/management/service-worker.js +// -*- coding: utf-8 -*- +console.log('Service Worker Loaded and Ready.'); + +// CACHE_NAME را برای مدیریت نسخه‌ها تغییر دهید +const CACHE_NAME = 'marzban-pwa-cache-v1.0'; + +// فایل‌هایی که باید در زمان نصب Service Worker کش شوند +const urlsToCache = [ + '/', + '/dashboard/', + '/static/js/main.js', + '/static/css/styles.css', + '/static/images/icons/icon-192x192.png', + '/static/images/icons/icon-512x512.png', + '/manifest.json' +]; + +self.addEventListener('install', (event) => { + console.log('Service Worker: Install event.'); + // در زمان نصب، فایل‌های اصلی را در حافظه کش ذخیره می‌کنیم + event.waitUntil( + caches.open(CACHE_NAME) + .then((cache) => { + console.log('Opened cache'); + return cache.addAll(urlsToCache); + }) + ); +}); + +self.addEventListener('activate', (event) => { + console.log('Service Worker: Activate event.'); + // پاک کردن کش‌های قدیمی + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + console.log('Service Worker: Deleting old cache: ', cacheName); + return caches.delete(cacheName); + } + }) + ); + }) + ); +}); + +self.addEventListener('fetch', (event) => { + // برای درخواست‌های شبکه، ابتدا کش را بررسی می‌کنیم و در صورت عدم وجود، درخواست شبکه می‌زنیم + event.respondWith( + caches.match(event.request).then((response) => { + // اگر در کش بود، همان را برمی‌گردانیم + if (response) { + return response; + } + // اگر در کش نبود، درخواست شبکه می‌زنیم + return fetch(event.request); + }) + ); +}); + +self.addEventListener('push', (event) => { + const data = event.data.json(); + console.log('Push received...', data); + + const title = data.head || 'نوتیفیکیشن جدید'; + const options = { + body: data.body, + icon: data.icon || '/static/images/icons/icon-192x192.png', + // الگوی لرزش: 200ms لرزش، 100ms مکث، 200ms لرزش + vibrate: data.vibrate || [200, 100, 200], + // پخش صدای پیش‌فرض سیستم + // توجه: پخش فایل صوتی سفارشی در مرورگرها به طور کامل پشتیبانی نمی‌شود. + // مرورگرها از صدای پیش‌فرض سیستم برای نوتیفیکیشن استفاده می‌کنند. + sound: data.sound || undefined, + badge: '/static/images/icons/badge-72x72.png' + }; + + event.waitUntil( + self.registration.showNotification(title, options) + ); +}); + +self.addEventListener('notificationclick', (event) => { + console.log('Notification clicked', event); + event.notification.close(); + event.waitUntil( + clients.openWindow('https://your-panel-address.com/dashboard/') + ); +}); + diff --git a/management/static/manifest.json b/management/static/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..8bbc39113ce1f19d0962e90a9a0d03e10be70c32 --- /dev/null +++ b/management/static/manifest.json @@ -0,0 +1,23 @@ +// management/static/manifest.json +{ + "name": "پنل مدیریت مرزبان", + "short_name": "مرزبان", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#4f46e5", + "description": "پنل مدیریت پیشرفته برای مرزبان", + "icons": [ + { + "src": "/static/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/static/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} + diff --git a/management/static/styles.css b/management/static/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..d6e60313377e679b0787a4dd4b983cff334d9cd7 --- /dev/null +++ b/management/static/styles.css @@ -0,0 +1,22 @@ +/* management/static/css/styles.css */ +@font-face { + font-family: 'Vazirmatn'; + src: url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.0.3/fonts/web/eot/Vazirmatn-Regular.eot') format('eot'), + url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.0.3/fonts/web/woff2/Vazirmatn-Regular.woff2') format('woff2'), + url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.0.3/fonts/web/woff/Vazirmatn-Regular.woff') format('woff'), + url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.0.3/fonts/web/ttf/Vazirmatn-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +body { + font-family: 'Vazirmatn', sans-serif; +} + +/* Custom styles for the configs list */ +#configs-list li:hover { + background-color: #f1f5f9; + transition: background-color 0.2s ease-in-out; +} + diff --git a/management/tasks.py b/management/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..06bc1e4358bc1a27b6271c1cf2d58875237c25d7 --- /dev/null +++ b/management/tasks.py @@ -0,0 +1,192 @@ +# management/tasks.py +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals +import requests +import json +import os +import datetime +from celery import shared_task +from django.db import connections, connection +from django.conf import settings +from django.utils import timezone + +from .models import ( + Panel, + AdminLevel2, + AdminLevel3, + EndUser, + Payment, + CustomUser, + Subscription, +) +from django.core.management import call_command + +# --- Utilities --- +# FIXED: Renamed function to match imports in other files +@shared_task +def send_telegram_message(chat_id, message, bot_token): + """ + Sends a message to a specific Telegram chat using a bot token. + """ + if not bot_token: + print("Telegram bot token is not configured.") + return + + url = f"https://api.telegram.org/bot{bot_token}/sendMessage" + payload = { + "chat_id": chat_id, + "text": message, + "parse_mode": "Markdown" + } + + try: + response = requests.post(url, json=payload) + response.raise_for_status() + print(f"Notification sent successfully to chat_id {chat_id}") + except requests.RequestException as e: + print(f"Failed to send notification to chat_id {chat_id}: {e}") + +# --- Celery Tasks --- +@shared_task(name="management.tasks.schedule_reminders") +def schedule_reminders(): + """ + Celery task to send periodic reminders to admins and users. + """ + now = timezone.now().date() + + # 1. License expiry warnings for AdminLevel2s (resellers) + three_days_from_now = now + datetime.timedelta(days=3) + admin_level2s = AdminLevel2.objects.filter(license_expiry_date__lte=three_days_from_now) + for admin in admin_level2s: + # Assuming AdminLevel2 is linked to a Panel through its CustomUser owner + # This part might need adjustment based on the final model structure + try: + panel = Panel.objects.get(owner=admin.user) + if admin.telegram_chat_id and panel.telegram_bot_token: + message = ( + f"🚨 **هشدار انقضای لایسنس!**\n\n" + f"ادمین {admin.user.username}، لایسنس شما در تاریخ {admin.license_expiry_date} منقضی خواهد شد. لطفاً برای تمدید آن اقدام کنید." + ) + send_telegram_message( + chat_id=admin.telegram_chat_id, + message=message, + bot_token=panel.telegram_bot_token + ) + except Panel.DoesNotExist: + print(f"Panel for admin {admin.user.username} not found.") + + +@shared_task +def send_payment_receipt_to_admin(payment_id): + """ + Sends a payment receipt notification to the relevant admin via Telegram. + """ + try: + payment = Payment.objects.get(id=payment_id) + admin = payment.admin + panel = payment.subscription.panel + + if not admin.admin_level_3.telegram_chat_id or not panel.telegram_bot_token: + print(f"Admin telegram_chat_id or bot token not found for payment {payment_id}.") + return + + message = ( + f"🔔 **رسید پرداخت جدید!**\n\n" + f"یک کاربر ({payment.subscription.end_user.username}) رسید پرداختی را برای تمدید اشتراک ارسال کرده است.\n" + f"مبلغ: {payment.amount} تومان\n" + f"توکن پرداخت: `{payment.payment_token}`\n" + f"لطفا به پنل خود مراجعه کرده و پرداخت را تایید کنید." + ) + + if payment.receipt_image: + message += f"\n\nتصویر رسید پرداخت آپلود شده است." + elif payment.receipt_text: + message += f"\n\nمتن رسید پرداخت: {payment.receipt_text}" + + send_telegram_message( + chat_id=admin.admin_level_3.telegram_chat_id, + message=message, + bot_token=panel.telegram_bot_token + ) + + except Payment.DoesNotExist: + print(f"Payment with id {payment_id} not found.") + except Exception as e: + print(f"Error sending payment receipt to admin: {e}") + + +@shared_task +def send_payment_confirmation_to_user(end_user_id, amount, status): + """ + Sends a payment confirmation message to the end-user. + """ + try: + end_user = EndUser.objects.get(id=end_user_id) + panel = end_user.panel + + if not panel or not panel.telegram_bot_token or not end_user.telegram_chat_id: + print(f"EndUser panel, bot token or telegram_chat_id not found for user {end_user_id}.") + return + + if status == 'approved': + message = ( + f"✅ **پرداخت شما تأیید شد**\n\n" + f"مبلغ {amount} تومان برای پنل {panel.name} با موفقیت تأیید شد. سرویس شما به زودی فعال خواهد شد." + ) + else: + message = ( + f"❌ **پرداخت شما رد شد**\n\n" + f"پرداخت {amount} تومان برای پنل {panel.name} رد شد. لطفاً رسید معتبر را ارسال کنید." + ) + + send_telegram_message( + chat_id=end_user.telegram_chat_id, + message=message, + bot_token=panel.telegram_bot_token + ) + except EndUser.DoesNotExist: + print(f"EndUser with id {end_user_id} not found.") + except Exception as e: + print(f"Error sending payment confirmation to user: {e}") + +@shared_task(name="management.tasks.check_subscription_expiry") +def check_subscription_expiry(): + """ + Celery task to check subscription expiry and send notifications. + """ + now = timezone.now().date() + + # Send reminder 3 days before expiry + three_days_from_now = now + datetime.timedelta(days=3) + expiring_soon_subscriptions = Subscription.objects.filter( + end_date__date=three_days_from_now, + status='active' + ) + + for sub in expiring_soon_subscriptions: + if sub.end_user.telegram_chat_id and sub.panel.telegram_bot_token: + message = ( + f"⏳ **اشتراک شما به زودی منقضی می‌شود!**\n\n" + f"کاربر {sub.end_user.username}، اشتراک شما در تاریخ {sub.end_date.strftime('%Y-%m-%d')} منقضی خواهد شد. برای تمدید آن از طریق بات اقدام کنید." + ) + send_telegram_message( + chat_id=sub.end_user.telegram_chat_id, + message=message, + bot_token=sub.panel.telegram_bot_token + ) + + # Deactivate expired subscriptions + expired_subscriptions = Subscription.objects.filter(end_date__lte=now, status='active') + for sub in expired_subscriptions: + sub.status = 'expired' + sub.save() + if sub.end_user.telegram_chat_id and sub.panel.telegram_bot_token: + message = ( + f"❌ **اشتراک شما منقضی شد!**\n\n" + f"کاربر {sub.end_user.username}، اشتراک شما منقضی شده است. لطفا برای تمدید اقدام کنید." + ) + send_telegram_message( + chat_id=sub.end_user.telegram_chat_id, + message=message, + bot_token=sub.panel.telegram_bot_token + ) diff --git a/management/telebot.py b/management/telebot.py new file mode 100644 index 0000000000000000000000000000000000000000..2fa4f4190deabcd41707ebaa50b59ea1c63c3a1a --- /dev/null +++ b/management/telebot.py @@ -0,0 +1,62 @@ +# management/telebot.py +# -*- coding: utf-8 -*- +import logging +from django.contrib.auth import get_user_model +from django.conf import settings +from .models import TelegramUser +import requests +import json + +logger = logging.getLogger(__name__) +CustomUser = get_user_model() + +def send_telegram_message(chat_id, message): + """ + ارسال پیام به یک کاربر تلگرام با استفاده از API. + """ + TOKEN = settings.TELEGRAM_BOT_TOKEN + API_URL = f"https://api.telegram.org/bot{TOKEN}/sendMessage" + payload = { + 'chat_id': chat_id, + 'text': message + } + try: + response = requests.post(API_URL, json=payload) + response.raise_for_status() + logger.info(f"Message sent to chat ID {chat_id}: {message}") + except requests.exceptions.RequestException as e: + logger.error(f"Error sending message to Telegram: {e}") + +def process_telegram_message(update): + """ + پردازش پیام‌های ورودی از تلگرام. + """ + message_data = update.get('message') + if not message_data: + return + + chat_id = message_data['chat']['id'] + text = message_data.get('text', '') + + # بررسی اگر متن یک کد امنیتی است + # اینجا یک شرط برای طول توکن میگذاریم تا از بررسی پیام‌های کوتاه جلوگیری شود + if 10 < len(text) < 50: + # ارسال کد به سرور برای تأیید + payload = { + 'token': text, + 'chat_id': chat_id + } + try: + # URL باید به آدرس واقعی API در سرور شما اشاره کند + response = requests.post('http://127.0.0.1:8000/api/telegram/verify-token/', json=payload) + result = response.json() + send_telegram_message(chat_id, result['message']) + except requests.exceptions.RequestException as e: + logger.error(f"Error communicating with Django server: {e}") + send_telegram_message(chat_id, "متاسفم، در حال حاضر نمی‌توانم درخواست شما را پردازش کنم. لطفاً بعداً امتحان کنید.") + + elif text == '/start': + send_telegram_message(chat_id, "سلام، برای همگام‌سازی حساب خود، لطفاً یک کد امنیتی از پنل کاربری خود دریافت کرده و آن را برای من ارسال کنید.") + else: + send_telegram_message(chat_id, "دستور شما نامعتبر است. لطفاً کد امنیتی خود را ارسال کنید یا از دستورات مجاز استفاده کنید.") + diff --git a/management/templates/management/%} b/management/templates/management/%} new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/management/templates/management/%} @@ -0,0 +1 @@ + diff --git a/management/templates/management/admin-panel-management.html b/management/templates/management/admin-panel-management.html new file mode 100644 index 0000000000000000000000000000000000000000..b765a1258d0d07870483229c6f9a0f552389641e --- /dev/null +++ b/management/templates/management/admin-panel-management.html @@ -0,0 +1,276 @@ + + + + + + مدیریت پنل‌های Marzban + + + + + +
+

مدیریت پنل‌های Marzban

+ + +
+

افزودن پنل جدید

+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ + +

لیست پنل‌های موجود

+
+ + + + + + + + + + + + + +
نامآدرس پنلنام دیتابیسپیش‌فرضعملیات
+
+
+ + + + + diff --git a/management/templates/management/admin_dashboard.html b/management/templates/management/admin_dashboard.html new file mode 100644 index 0000000000000000000000000000000000000000..2f0e4169ac3bdcc72f0d9ec9f7ea98b03f9b235d --- /dev/null +++ b/management/templates/management/admin_dashboard.html @@ -0,0 +1,35 @@ +{% extends 'management/base.html' %} +{% load static %} + +{% block header_title %}داشبورد ادمین {{ request.user.username }}{% endblock %} + +{% block content %} +
+
+
+
+
تعداد کاربران
+

{{ total_users }}

+
+
+
+
+
+
+
پرداخت‌های در انتظار تأیید
+

{{ pending_payments }}

+
+
+
+
+
+
+
کاربران بدون پرداخت (۴۸ ساعت)
+

{{ users_no_payment_48h }}

+
+
+
+
+{% endblock %} + + diff --git a/management/templates/management/admin_login.html b/management/templates/management/admin_login.html new file mode 100644 index 0000000000000000000000000000000000000000..a8e90db167ea4efd8e312a09add13ec28d26f5f2 --- /dev/null +++ b/management/templates/management/admin_login.html @@ -0,0 +1,112 @@ + + + + + + ورود به پنل ادمین + + + +
+

ورود به پنل ادمین

+ {% if messages %} + + {% endif %} +
+ {% csrf_token %} +
+ + +
+
+ + +
+ +
+
+ + + diff --git a/management/templates/management/amin_login.html b/management/templates/management/amin_login.html new file mode 100644 index 0000000000000000000000000000000000000000..a74294eddee88e2b4d6c6b032461b551a9cefa2a --- /dev/null +++ b/management/templates/management/amin_login.html @@ -0,0 +1,44 @@ + + + + + + ورود ادمین + + + + +
+

ورود به پنل ادمین

+ + {% if messages %} + + {% endif %} + +
+ {% csrf_token %} +
+ + {{ form.username }} +
+
+ + {{ form.password }} +
+ +
+
+ + + diff --git a/management/templates/management/base.html b/management/templates/management/base.html new file mode 100644 index 0000000000000000000000000000000000000000..d1ea7c46b90761d169639252a6f74259f71c79a8 --- /dev/null +++ b/management/templates/management/base.html @@ -0,0 +1,25 @@ + + + + + + + + {% block title %}پنل مدیریت{% endblock %} + + + + + + +
+ {% block content %}{% endblock %} +
+ + + diff --git a/management/templates/management/create_discount_code.html b/management/templates/management/create_discount_code.html new file mode 100644 index 0000000000000000000000000000000000000000..74fddc99976890938eddd7db1b604dc56b50e755 --- /dev/null +++ b/management/templates/management/create_discount_code.html @@ -0,0 +1,62 @@ + + + + + + + + ایجاد کد تخفیف + + + + +
+
+

ایجاد کد تخفیف

+
+ +
+ {% if messages %} +
    + {% for message in messages %} +
  • + {{ message }} +
  • + {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} +
+ {% for field in form %} +
+ +
+ {{ field }} +
+ {% if field.errors %} + {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endif %} +
+ {% endfor %} +
+ +
+ +
+
+
+
+ + + diff --git a/management/templates/management/create_notification.html b/management/templates/management/create_notification.html new file mode 100644 index 0000000000000000000000000000000000000000..f8578685f91bbde75e6d78bf7dd0b4b5bc41bc06 --- /dev/null +++ b/management/templates/management/create_notification.html @@ -0,0 +1,198 @@ +{% extends 'management/base.html' %} +{% load static %} + +{% block header_title %}ارسال اعلان جدید{% endblock %} + +{% block content %} +
+
+
ارسال اعلان جدید
+ تاریخچه اعلان‌ها +
+
+
+ {% csrf_token %} + + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+
گزینه‌های پیشرفته (اختیاری)
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+ +
+ + +
+
+ + +
+
+
+ + +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ + + + + +
+
+
+ +{% endblock %} + diff --git a/management/templates/management/error.html b/management/templates/management/error.html new file mode 100644 index 0000000000000000000000000000000000000000..541925880dc4849891fd3bef47bdcd23bfe06533 --- /dev/null +++ b/management/templates/management/error.html @@ -0,0 +1,43 @@ + + + + + + خطا + + + +
+

خطا!

+

{{ message }}

+ بازگشت به صفحه اصلی +
+ + diff --git a/management/templates/management/home.html b/management/templates/management/home.html new file mode 100644 index 0000000000000000000000000000000000000000..299e23234162bfa1e8dc1cb0b912f3c4953f8b65 --- /dev/null +++ b/management/templates/management/home.html @@ -0,0 +1,30 @@ + + + + + + صفحه اصلی + + + + +
+

به پنل خوش آمدید

+

لطفاً نوع کاربری خود را انتخاب کنید.

+ +
+ + + diff --git a/management/templates/management/login.html b/management/templates/management/login.html new file mode 100644 index 0000000000000000000000000000000000000000..d6dd882d96d25e578c6f54c4265d4dd642a34638 --- /dev/null +++ b/management/templates/management/login.html @@ -0,0 +1,87 @@ + + + + + ورود به پنل ادمین + + + +
+

ورود به پنل ادمین

+ {% if error %} +

{{ error }}

+ {% endif %} +
+ {% csrf_token %} +
+ + +
+
+ + +
+ +
+
+ + diff --git a/management/templates/management/notification_history.html b/management/templates/management/notification_history.html new file mode 100644 index 0000000000000000000000000000000000000000..35a8b5888e8b21d71cb42c8a6fd159a4a9c1410a --- /dev/null +++ b/management/templates/management/notification_history.html @@ -0,0 +1,41 @@ +{% extends 'management/base.html' %} + +{% block header_title %}اعلان‌ها و اخطارها{% endblock %} + +{% block content %} +
+
+
+
+
+
لیست اعلان‌ها
+
+
+ {% for notification in notifications %} + + {% empty %} + + {% endfor %} +
+
+
+
+
+{% endblock %} + diff --git a/management/templates/management/notifications.html b/management/templates/management/notifications.html new file mode 100644 index 0000000000000000000000000000000000000000..35a8b5888e8b21d71cb42c8a6fd159a4a9c1410a --- /dev/null +++ b/management/templates/management/notifications.html @@ -0,0 +1,41 @@ +{% extends 'management/base.html' %} + +{% block header_title %}اعلان‌ها و اخطارها{% endblock %} + +{% block content %} +
+
+
+
+
+
لیست اعلان‌ها
+
+
+ {% for notification in notifications %} + + {% empty %} + + {% endfor %} +
+
+
+
+
+{% endblock %} + diff --git a/management/templates/management/panel_owner_dashboard b/management/templates/management/panel_owner_dashboard new file mode 100644 index 0000000000000000000000000000000000000000..be678f8b18de6f6c2169d6d70b6886ecab6543d6 --- /dev/null +++ b/management/templates/management/panel_owner_dashboard @@ -0,0 +1,592 @@ + + + + + + داشبورد مدیر پنل + + + + + +
+
+

داشبورد مدیر پنل

+
+ + +
+
+ + +
+

مدیریت پنل‌ها

+
+ + + + + + + + + + + + + + + +
نامآدرسدیتابیسپیش‌فرضعملیات
در حال بارگذاری پنل‌ها...
+
+ +
+ +
+ + + +
+ + +
+

مدیریت ادمین‌های زیرمجموعه

+
+ + + + + + + + + + + + + + +
IDنام کاربریسطح دسترسیعملیات
در حال بارگذاری ادمین‌ها...
+
+ +
+ +
+ + + +
+ + +
+

اعلان‌های تلگرام

+

برای دریافت اعلان‌ها از پنل خود در تلگرام، یک توکن ایجاد کرده و آن را برای ربات تلگرام ارسال کنید.

+ +
+ +
+ +
+ +
+ + +
+ +
+ + + + + + diff --git a/management/templates/management/panel_owner_dashboard.html b/management/templates/management/panel_owner_dashboard.html new file mode 100644 index 0000000000000000000000000000000000000000..8a1a1b74e299154c7e8311f1e6d2d80d2f04cf22 --- /dev/null +++ b/management/templates/management/panel_owner_dashboard.html @@ -0,0 +1,341 @@ + + + + + + داشبورد مدیر پنل + + + + + + +
+
+

داشبورد مدیر پنل

+
+ + +
+
+ + +
+

مدیریت پنل‌ها

+ + +
+

افزودن پنل جدید

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ +

لیست پنل‌های من

+
+ + + + + + + + + + + + + + + + +
ID پنلنامآدرسدیتابیسپیش‌فرضعملیات
در حال بارگذاری پنل‌ها...
+
+
+
+ + + + + + diff --git a/management/templates/management/payment_form.html b/management/templates/management/payment_form.html new file mode 100644 index 0000000000000000000000000000000000000000..b5becad9ecbe41d55a6411643269d0f3d53cc340 --- /dev/null +++ b/management/templates/management/payment_form.html @@ -0,0 +1,80 @@ + + + + + + فرم پرداخت + + + +
+

فرم ثبت پرداخت

+
+ {% csrf_token %} +
+ + +
+
+ + +
+ +
+
+ + diff --git a/management/templates/management/payment_management.html b/management/templates/management/payment_management.html new file mode 100644 index 0000000000000000000000000000000000000000..a31439f2d14de6455540ca70891f47be8de1061b --- /dev/null +++ b/management/templates/management/payment_management.html @@ -0,0 +1,46 @@ +{% extends 'management/base.html' %} + +{% block header_title %}واریزی‌های در انتظار تأیید{% endblock %} + +{% block content %} +
+
+
واریزی‌های در انتظار تأیید
+
+
+ {% if pending_payments_list %} + + + + + + + + + + + + + {% for payment in pending_payments_list %} + + + + + + + + + {% endfor %} + +
نام کاربرمبلغپلنتاریخ ارسالرسید واریزعملیات
{{ payment.user.full_name }}{{ payment.amount|floatformat:0 }}{{ payment.plan.name }}{{ payment.created_at|date:"Y/m/d - H:i" }}مشاهده + تأیید +
+ {% else %} + + {% endif %} +
+
+{% endblock %} + diff --git a/management/templates/management/payment_success.html b/management/templates/management/payment_success.html new file mode 100644 index 0000000000000000000000000000000000000000..2ea661db476fca17145d49e290ffd7f13f802a2c --- /dev/null +++ b/management/templates/management/payment_success.html @@ -0,0 +1,32 @@ +{% load static %} + + + + + + پرداخت موفق + + + + +
+
+ + + +
+

پرداخت شما با موفقیت ثبت شد!

+

درخواست شما برای ادمین ارسال شد و پس از تأیید، اشتراک شما فعال خواهد شد.

+ + بازگشت به داشبورد + +
+ + + diff --git a/management/templates/management/plan_management.html b/management/templates/management/plan_management.html new file mode 100644 index 0000000000000000000000000000000000000000..bb23256c1c52f5a91fcbe9e4f79f33672e0f3c7c --- /dev/null +++ b/management/templates/management/plan_management.html @@ -0,0 +1,77 @@ +{% extends 'management/base.html' %} + +{% block header_title %}مدیریت پلن‌ها{% endblock %} + +{% block content %} +
+
+
ایجاد پلن جدید
+
+
+
+ {% csrf_token %} +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
لیست پلن‌ها
+
+
+ + + + + + + + + + + + {% for plan in plans %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
نام پلنقیمتمدتحجمعملیات
{{ plan.name }}{{ plan.price|floatformat:0 }}{{ plan.duration }} روز{{ plan.volume }} گیگ + ویرایش +
+ {% csrf_token %} + +
+
پلنی ثبت نشده است.
+
+
+{% endblock %} + diff --git a/management/templates/management/profile.html b/management/templates/management/profile.html new file mode 100644 index 0000000000000000000000000000000000000000..8936635910b2198fee652fcc339a53bc8bcabf4c --- /dev/null +++ b/management/templates/management/profile.html @@ -0,0 +1,50 @@ + + + + + + پروفایل کاربری + + + + +
+
+
+
+

خوش آمدید، {{ request.user.username }}!

+ + {% if current_subscription %} +

اشتراک فعلی شما: {{ current_subscription.plan.name }}

+

تاریخ انقضا: {{ current_subscription.end_date }}

+

وضعیت: {% if current_subscription.is_active %}فعال{% else %}غیرفعال{% endif %}

+ {% else %} +

شما هیچ اشتراکی ندارید.

+ {% endif %} + +
+ + + + خروج از حساب +
+
+
+
+ + diff --git a/management/templates/management/submit_payment.html b/management/templates/management/submit_payment.html new file mode 100644 index 0000000000000000000000000000000000000000..f54060332b62497f828c2f9b5953a78fb9ab6424 --- /dev/null +++ b/management/templates/management/submit_payment.html @@ -0,0 +1,83 @@ +{% load static %} + + + + + + ثبت پرداخت + + + + +
+

ثبت پرداخت برای پلن "{{ plan.name }}"

+ + {% if messages %} + + {% endif %} + +

لطفاً پس از پرداخت، رسید را در این قسمت آپلود کنید. پس از تأیید ادمین، اشتراک شما فعال خواهد شد.

+ +
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ + + diff --git a/management/templates/management/super_admin_dashboard.html b/management/templates/management/super_admin_dashboard.html new file mode 100644 index 0000000000000000000000000000000000000000..273f44ac33d97b7a159c538a8908d15df582e813 --- /dev/null +++ b/management/templates/management/super_admin_dashboard.html @@ -0,0 +1,325 @@ + + + + + + داشبورد سوپر ادمین + + + + + + +
+
+

داشبورد سوپر ادمین

+
+ خوش آمدید، سوپر ادمین + +
+
+ + +
+

مدیریت لایسنس‌ها

+ + +
+

ایجاد لایسنس جدید

+
+
+ + +
+ +
+
+ +

لیست لایسنس‌ها

+
+ + + + + + + + + + + + + + + + + +
ID لایسنسکلید لایسنسID مالکوضعیتپنل متصلتاریخ ایجادعملیات
در حال بارگذاری لایسنس‌ها...
+
+
+
+ + + + + + diff --git a/management/templates/management/update_plan.html b/management/templates/management/update_plan.html new file mode 100644 index 0000000000000000000000000000000000000000..746d08e2618790f86ea9833fb8d141f442c58d63 --- /dev/null +++ b/management/templates/management/update_plan.html @@ -0,0 +1,39 @@ +{% extends 'management/base.html' %} + +{% block header_title %}ویرایش پلن{% endblock %} + +{% block content %} +
+
+
ویرایش پلن: {{ plan.name }}
+
+
+
+ {% csrf_token %} +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + بازگشت +
+
+
+{% endblock %} + diff --git a/management/templates/management/user_buy_plan.html b/management/templates/management/user_buy_plan.html new file mode 100644 index 0000000000000000000000000000000000000000..b826e76aac1ba515270d762282ffa94192cfea7c --- /dev/null +++ b/management/templates/management/user_buy_plan.html @@ -0,0 +1,55 @@ +{% load static %} + + + + + + خرید پلن + + + + +
+
+

خرید پلن جدید

+ بازگشت به داشبورد +
+ + {% if messages %} + + {% endif %} + + {% if plans %} +
+ {% for plan in plans %} +
+

{{ plan.name }}

+

{{ plan.duration_months }} ماه - {{ plan.data_limit_gb }} گیگابایت

+
{{ plan.price }} تومان
+ + انتخاب و پرداخت + +
+ {% endfor %} +
+ {% else %} +
+

در حال حاضر هیچ پلنی برای خرید موجود نیست.

+
+ {% endif %} +
+ + + diff --git a/management/templates/management/user_dashboard.html b/management/templates/management/user_dashboard.html new file mode 100644 index 0000000000000000000000000000000000000000..913bf7cba0c26b2342167be942caff52ecf96c6b --- /dev/null +++ b/management/templates/management/user_dashboard.html @@ -0,0 +1,74 @@ + + + + + + + + داشبورد کاربر + + + + + + +
+
+

داشبورد کاربر

+

خوش آمدید، ...!

+
+ +
+
+
+

وضعیت سرویس: ...

+
+
+ +
+
+

حجم مصرفی

+

...

+
+
+

حجم کل

+

...

+
+
+ +
+

کانفیگ‌ها

+
    + +
+
+ + +
+

همگام‌سازی با ربات تلگرام

+
+ + +
+
+ + + +
+
+ + + + + + + diff --git a/management/templates/management/user_links.html b/management/templates/management/user_links.html new file mode 100644 index 0000000000000000000000000000000000000000..78ed26bf5481cf14fa6b5da7aa296bca112e6049 --- /dev/null +++ b/management/templates/management/user_links.html @@ -0,0 +1,190 @@ + + + + + + + لینک‌های اشتراک + + + + + + +
+

لینک‌های اشتراک شما

+ + {% if subscription_url %} + + {% endif %} + + {% if config_links %} + + {% endif %} + + {% if not subscription_url and not config_links %} +

هیچ لینک اشتراکی برای شما یافت نشد.

+ {% endif %} + +
+
+ × +

بارکد QR

+
+
+
+
+ + + + + diff --git a/management/templates/management/user_login.html b/management/templates/management/user_login.html new file mode 100644 index 0000000000000000000000000000000000000000..63bf0ed10ffae82210c53e9d0239be4c4608475e --- /dev/null +++ b/management/templates/management/user_login.html @@ -0,0 +1,110 @@ + + + + + + ورود به پنل کاربری + + + +
+

ورود کاربر

+ + {% if messages %} + + {% endif %} + +
+ {% csrf_token %} +
+ + +
+ +
+
+ + + diff --git a/management/templates/management/user_management.html b/management/templates/management/user_management.html new file mode 100644 index 0000000000000000000000000000000000000000..e567ef9081b2d4c467172ace302854ab84a7904f --- /dev/null +++ b/management/templates/management/user_management.html @@ -0,0 +1,81 @@ + + + + + + + + مدیریت کاربران + + + + +
+
+

پنل مدیریت کاربران

+
+ +
+ {% if messages %} +
    + {% for message in messages %} +
  • + {{ message }} +
  • + {% endfor %} +
+ {% endif %} + +

لیست کاربران من

+ + {% if users %} +
+ + + + + + + + + + + + {% for user in users %} + + + + + + + + {% endfor %} + +
نام کاربریوضعیتحجم مصرفیحجم کلعملیات
{{ user.username }} + + {{ user.status }} + + {{ user.used_traffic|default:"0"|floatformat:2 }} GB{{ user.total_traffic|default:"0"|floatformat:2 }} GB + + + + +
+
+ {% else %} +

هیچ کاربری برای مدیریت وجود ندارد.

+ {% endif %} +
+
+ + + diff --git a/management/templates/management/user_profile.html b/management/templates/management/user_profile.html new file mode 100644 index 0000000000000000000000000000000000000000..b0e8b97c604524e22ca0300ad45d251687e2b57c --- /dev/null +++ b/management/templates/management/user_profile.html @@ -0,0 +1,33 @@ + + + + + + پروفایل کاربری + + + +
+

پروفایل کاربری

+ + خروج از حساب +
+ + diff --git a/management/tests.py b/management/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/management/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/management/urls.py b/management/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..f727f78c01d514a5b2f2bde03d151b4ebaabbc9e --- /dev/null +++ b/management/urls.py @@ -0,0 +1,74 @@ +# management/urls.py +# -*- coding: utf-8 -*- +from django.urls import path, include +from rest_framework_simplejwt.views import TokenRefreshView +from rest_framework.routers import DefaultRouter +from .views import ( + CustomTokenObtainPairView, + LogoutAPIView, + TelegramWebhookView, + PushSubscriptionAPIView, + GenerateTelegramTokenAPIView, + VerifyTelegramTokenAPIView, + PanelViewSet, + LicenseViewSet, + MarzbanAdminViewSet, + PlanViewSet, + EndUserViewSet, + SubscriptionViewSet, + PaymentViewSet, + PaymentReceiptUploadAPIView, + # New views for payment system + PaymentMethodViewSet, + PaymentDetailAPIView, + EndUserPaymentOptionsAPIView, + # New views that were recently added + AdminLevel3SubscriptionViewSet, + PurchaseSubscriptionForUserAPIView, +) + +# Create a router and register our viewsets with it. +router = DefaultRouter() +router.register(r'panels', PanelViewSet, basename='panel') +router.register(r'licenses', LicenseViewSet, basename='license') +router.register(r'plans', PlanViewSet, basename='plan') +router.register(r'endusers', EndUserViewSet, basename='enduser') +router.register(r'subscriptions', SubscriptionViewSet, basename='subscription') +router.register(r'payments', PaymentViewSet, basename='payment') +# New: ViewSet for managing payment methods by Admin Level 2 +router.register(r'payment-methods', PaymentMethodViewSet, basename='payment-method') +# New: ViewSet for Admin Level 3 to manage subscriptions +router.register(r'admin/subscriptions', AdminLevel3SubscriptionViewSet, basename='admin-subscription') + +# The MarzbanAdminViewSet is a custom ViewSet, so we define its paths manually. +marzban_admin_list = MarzbanAdminViewSet.as_view({ + 'get': 'list', + 'post': 'create' +}) +marzban_admin_detail = MarzbanAdminViewSet.as_view({ + 'delete': 'destroy' +}) + +urlpatterns = [ + # API endpoints + path('api/token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'), + path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('api/logout/', LogoutAPIView.as_view(), name='logout'), + # Use the router to handle all ViewSet API endpoints + path('api/', include(router.urls)), + # Marzban Admin Management (manual routing for a custom ViewSet) + path('api/marzban-admins/', marzban_admin_list, name='api_marzban_admin_list'), + path('api/marzban-admins//', marzban_admin_detail, name='api_marzban_admin_detail'), + # Telegram & Push notifications + path('telegram-webhook//', TelegramWebhookView.as_view(), name='telegram_webhook'), + path('api/push-subscription/', PushSubscriptionAPIView.as_view(), name='push_subscription'), + path('api/telegram-token/', GenerateTelegramTokenAPIView.as_view(), name='generate_telegram_token'), + path('api/verify-telegram-token/', VerifyTelegramTokenAPIView.as_view(), name='verify_telegram_token'), + path('api/upload-receipt/', PaymentReceiptUploadAPIView.as_view(), name='upload_receipt'), + # New: Payment System URLs + path('api/admin/payment-details/', PaymentDetailAPIView.as_view(), name='admin_payment_details'), + path('api/enduser/payment-options/', EndUserPaymentOptionsAPIView.as_view(), name='enduser_payment_options'), + # New: URL for EndUser to purchase subscriptions for other users + path('api/purchase-subscription/', PurchaseSubscriptionForUserAPIView.as_view(), name='purchase_subscription'), +] + diff --git a/management/utils.py b/management/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..8eaee8a270406e012d3c083d6be9979a88589b74 --- /dev/null +++ b/management/utils.py @@ -0,0 +1,8 @@ +# management/utils.py +import telegram + +def send_telegram_notification(payment_id): + # منطق ارسال نوتیفیکیشن تلگرام اینجا قرار می‌گیرد. + # برای شروع، می‌توانید این تابع را خالی بگذارید. + print(f"Sending notification for payment ID: {payment_id}") + pass diff --git a/management/views.py b/management/views.py new file mode 100644 index 0000000000000000000000000000000000000000..8da03c21ef0a20bc97600935c42d47d7f123b32f --- /dev/null +++ b/management/views.py @@ -0,0 +1,347 @@ +# management/views.py +# -*- coding: utf-8 -*- +from django.utils import timezone +from rest_framework import generics, viewsets, status +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated, IsAdminUser +from rest_framework.views import APIView +from rest_framework_simplejwt.views import TokenObtainPairView +from rest_framework_simplejwt.tokens import RefreshToken +import datetime + +from .models import ( + CustomUser, Panel, MarzbanAdmin, License, SecurityToken, Plan, Subscription, + Payment, EndUser, PaymentMethod, PaymentSetting, PaymentDetail, TelegramUser +) +from .serializers import ( + PanelSerializer, LicenseSerializer, MarzbanAdminSerializer, CustomUserSerializer, + SecurityTokenSerializer, PlanSerializer, SubscriptionSerializer, + ReceiptUploadSerializer, PaymentMethodSerializer, PaymentDetailSerializer, + EndUserPaymentDetailSerializer, AdminLevel3SubscriptionSerializer, + PurchaseSubscriptionForUserSerializer, +) +from .tasks import ( + send_telegram_message, send_payment_receipt_to_admin, + send_payment_confirmation_to_user, +) + +# --- Authentication Views --- +class CustomTokenObtainPairView(TokenObtainPairView): + def post(self, request, *args, **kwargs): + response = super().post(request, *args, **kwargs) + if response.status_code == 200: + user = CustomUser.objects.get(username=request.data['username']) + response.data['user_role'] = user.role + return response + +class LogoutAPIView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + refresh_token = request.data["refresh_token"] + token = RefreshToken(refresh_token) + token.blacklist() + return Response({"message": "Successfully logged out."}, status=status.HTTP_200_OK) + except Exception as e: + return Response({"error": "Invalid token or other error."}, status=status.HTTP_400_BAD_REQUEST) + +# --- ADDED: Missing ViewSets --- + +class PanelViewSet(viewsets.ModelViewSet): + serializer_class = PanelSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return Panel.objects.filter(owner=self.request.user) + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) + +class LicenseViewSet(viewsets.ModelViewSet): + serializer_class = LicenseSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + if user.is_super_admin(): + return License.objects.all() + return License.objects.filter(owner=user) + +class MarzbanAdminViewSet(viewsets.ModelViewSet): + serializer_class = MarzbanAdminSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + try: + panel = Panel.objects.get(owner=self.request.user) + return MarzbanAdmin.objects.filter(panel=panel) + except Panel.DoesNotExist: + return MarzbanAdmin.objects.none() + +class PlanViewSet(viewsets.ModelViewSet): + serializer_class = PlanSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + try: + panel = Panel.objects.get(owner=self.request.user) + return Plan.objects.filter(panel=panel) + except Panel.DoesNotExist: + return Plan.objects.none() + + def perform_create(self, serializer): + try: + panel = Panel.objects.get(owner=self.request.user) + serializer.save(panel=panel) + except Panel.DoesNotExist: + return Response({"error": "You do not own a panel."}, status=status.HTTP_403_FORBIDDEN) + +class EndUserViewSet(viewsets.ModelViewSet): + serializer_class = SubscriptionSerializer # A better serializer can be created for EndUser + permission_classes = [IsAuthenticated] + + def get_queryset(self): + if self.request.user.role == 'AdminLevel3': + try: + panel = Panel.objects.get(owner=self.request.user) + return EndUser.objects.filter(panel=panel) + except Panel.DoesNotExist: + return EndUser.objects.none() + return EndUser.objects.none() + +class SubscriptionViewSet(viewsets.ModelViewSet): + serializer_class = SubscriptionSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + if user.role == 'AdminLevel3': + try: + panel = Panel.objects.get(owner=user) + return Subscription.objects.filter(panel=panel) + except Panel.DoesNotExist: + return Subscription.objects.none() + elif user.role == 'EndUser': + try: + end_user = EndUser.objects.get(username=user.username) + return Subscription.objects.filter(end_user=end_user) + except EndUser.DoesNotExist: + return Subscription.objects.none() + return Subscription.objects.none() + +class PaymentViewSet(viewsets.ModelViewSet): + serializer_class = ReceiptUploadSerializer # A better serializer can be created for listing + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + if user.is_admin_level_3(): + return Payment.objects.filter(admin=user) + return Payment.objects.none() + +# --- Payment System Views --- +class PaymentMethodViewSet(viewsets.ModelViewSet): + queryset = PaymentMethod.objects.all() + serializer_class = PaymentMethodSerializer + permission_classes = [IsAuthenticated] + http_method_names = ['get', 'patch', 'put'] + + def get_queryset(self): + if self.request.user.is_authenticated and self.request.user.is_admin_level_2(): + return PaymentMethod.objects.all() + return PaymentMethod.objects.none() + +class PaymentDetailAPIView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + if not request.user.is_admin_level_3(): + return Response({'error': 'Permission denied.'}, status=status.HTTP_403_FORBIDDEN) + details, _ = PaymentDetail.objects.get_or_create(admin_level_3=request.user) + serializer = PaymentDetailSerializer(details) + return Response(serializer.data) + + def put(self, request): + if not request.user.is_admin_level_3(): + return Response({'error': 'Permission denied.'}, status=status.HTTP_403_FORBIDDEN) + details, _ = PaymentDetail.objects.get_or_create(admin_level_3=request.user) + serializer = PaymentDetailSerializer(details, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class EndUserPaymentOptionsAPIView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + if not request.user.role == 'EndUser': + return Response({'error': 'Permission denied.'}, status=status.HTTP_403_FORBIDDEN) + try: + end_user = EndUser.objects.get(username=request.user.username) + admin_level_3_user = end_user.panel.owner + payment_settings = PaymentSetting.objects.filter(admin_level_3=admin_level_3_user, is_active=True) + serializer = EndUserPaymentDetailSerializer(payment_settings, many=True) + return Response(serializer.data) + except EndUser.DoesNotExist: + return Response({'error': 'User not found.'}, status=status.HTTP_404_NOT_FOUND) + +# --- Subscription Management --- +class AdminLevel3SubscriptionViewSet(viewsets.ModelViewSet): + serializer_class = AdminLevel3SubscriptionSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + if self.request.user.is_authenticated and self.request.user.role == 'AdminLevel3': + try: + return Subscription.objects.filter(panel__owner=self.request.user) + except Panel.DoesNotExist: + return Subscription.objects.none() + return Subscription.objects.none() + + def perform_create(self, serializer): + serializer.save() + +# FIXED: Logic for purchasing subscription +class PurchaseSubscriptionForUserAPIView(APIView): + serializer_class = PurchaseSubscriptionForUserSerializer + permission_classes = [IsAuthenticated] + + def post(self, request, *args, **kwargs): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + data = serializer.validated_data + end_user_username = data.get('end_user_username') + plan_id = data.get('plan_id') + amount = data.get('amount') + + try: + # The user making the request must be an EndUser + if request.user.role != 'EndUser': + return Response({'error': 'Only EndUsers can purchase subscriptions.'}, status=status.HTTP_403_FORBIDDEN) + + # Get the panel of the user making the request + requester_end_user = EndUser.objects.get(username=request.user.username) + panel = requester_end_user.panel + admin_level_3_user = panel.owner + + plan = Plan.objects.get(id=plan_id, panel=panel) + + # Get or create the target end user within the same panel + target_end_user, _ = EndUser.objects.get_or_create( + username=end_user_username, defaults={'panel': panel} + ) + + subscription = Subscription.objects.create( + end_user=target_end_user, + plan=plan, + panel=panel, + status='pending', + ) + + payment = Payment.objects.create( + subscription=subscription, + admin=admin_level_3_user, + amount=amount, + status='pending', + ) + + return Response({ + "message": "Subscription request created successfully. Please upload the receipt.", + "subscription_id": subscription.id, + "payment_token": payment.payment_token + }, status=status.HTTP_201_CREATED) + + except EndUser.DoesNotExist: + return Response({'error': 'Requesting user profile not found.'}, status=status.HTTP_404_NOT_FOUND) + except Plan.DoesNotExist: + return Response({'error': 'Plan not found for your panel.'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +# --- Other Views --- + +# ADDED: PaymentReceiptUploadAPIView (was missing) +class PaymentReceiptUploadAPIView(APIView): + permission_classes = [IsAuthenticated] + serializer_class = ReceiptUploadSerializer + + def post(self, request, *args, **kwargs): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + try: + payment = Payment.objects.get(payment_token=serializer.validated_data['payment_token']) + + # Update payment with receipt details + payment.receipt_image = serializer.validated_data.get('receipt_image') + payment.receipt_text = serializer.validated_data.get('receipt_text') + payment.save() + + # Trigger celery task to notify admin + send_payment_receipt_to_admin.delay(payment.id) + + return Response({"message": "Receipt uploaded successfully. Please wait for admin approval."}, status=status.HTTP_200_OK) + except Payment.DoesNotExist: + return Response({"error": "Invalid payment token."}, status=status.HTTP_404_NOT_FOUND) + +# ADDED: GenerateTelegramTokenAPIView (was missing) +class GenerateTelegramTokenAPIView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + user = request.user + expiration = timezone.now() + datetime.timedelta(minutes=5) + # This assumes a direct relation between CustomUser and a Marzban Admin ID + if not user.marzban_admin_id: + return Response({'error': 'User is not linked to a Marzban admin.'}, status=status.HTTP_400_BAD_REQUEST) + + token, _ = SecurityToken.objects.update_or_create( + admin_id=str(user.marzban_admin_id), + defaults={'expiration_date': expiration} + ) + return Response({'token': token.token}) + +class VerifyTelegramTokenAPIView(APIView): + # This view is for unauthenticated users (the bot) to verify a token + permission_classes = [] + + def post(self, request): + token_value = request.data.get('token') + chat_id = request.data.get('chat_id') + + if not token_value or not chat_id: + return Response({'error': 'Token and chat_id are required.'}, status=status.HTTP_400_BAD_REQUEST) + + try: + token = SecurityToken.objects.get(token=token_value, expiration_date__gt=timezone.now()) + + # Logic to find the main DB CustomUser and update their telegram_chat_id + user = CustomUser.objects.get(marzban_admin_id=token.admin_id) + + # Assuming AdminLevel3 has a telegram_chat_id field + if hasattr(user, 'admin_level_3'): + user.admin_level_3.telegram_chat_id = chat_id + user.admin_level_3.save() + + token.delete() # One-time use token + return Response({'message': 'Telegram account linked successfully.'}) + except (SecurityToken.DoesNotExist, CustomUser.DoesNotExist): + return Response({'error': 'Invalid or expired token.'}, status=status.HTTP_400_BAD_REQUEST) + +class PushSubscriptionAPIView(APIView): + permission_classes = [IsAuthenticated] + def post(self, request, *args, **kwargs): + # Implementation needed + return Response({'message': 'Subscription created successfully.'}) + +# ADDED: TelegramWebhookView (was in urls.py but missing here) +class TelegramWebhookView(APIView): + permission_classes = [] + + def post(self, request, bot_token): + # Your telegram bot logic here + # print(request.data) + return Response({'status': 'ok'})