This view is limited to 50 files because it contains too many changes.  See the raw diff here.
Files changed (50) hide show
  1. key.py +32 -0
  2. manage.py +20 -0
  3. management/__init__.py +0 -0
  4. management/__pycache__/__init__.cpython-312.pyc +0 -0
  5. management/__pycache__/admin.cpython-312.pyc +0 -0
  6. management/__pycache__/apps.cpython-312.pyc +0 -0
  7. management/__pycache__/backends.cpython-312.pyc +0 -0
  8. management/__pycache__/forms.cpython-312.pyc +0 -0
  9. management/__pycache__/marzban_api.cpython-312.pyc +0 -0
  10. management/__pycache__/marzban_client.cpython-312.pyc +0 -0
  11. management/__pycache__/models.cpython-312.pyc +0 -0
  12. management/__pycache__/serializers.cpython-312.pyc +0 -0
  13. management/__pycache__/tasks.cpython-312.pyc +0 -0
  14. management/__pycache__/urls.cpython-312.pyc +0 -0
  15. management/__pycache__/utils.cpython-312.pyc +0 -0
  16. management/__pycache__/views.cpython-312.pyc +0 -0
  17. management/admin.py +130 -0
  18. management/backends.py +89 -0
  19. management/forms.py +13 -0
  20. management/manage.py +20 -0
  21. management/management/commands/backup_all_databases.py +100 -0
  22. management/management/commands/check_payments.py +42 -0
  23. management/management/commands/restore_all_databases.py +100 -0
  24. management/management/commands/schedule_reminders.py +203 -0
  25. management/marzban_client.py +79 -0
  26. management/migrations/0001_initial.py +241 -0
  27. management/migrations/__init__.py +0 -0
  28. management/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  29. management/migrations/__pycache__/0002_remove_user_admin_remove_payment_user_and_more.cpython-312.pyc +0 -0
  30. management/migrations/__pycache__/0003_notification_remove_payment_user_config_and_more.cpython-312.pyc +0 -0
  31. management/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  32. management/models.py +261 -0
  33. management/serializers.py +193 -0
  34. management/static/css/styles.css +22 -0
  35. management/static/js/main.js +174 -0
  36. management/static/js/service-worker.js +91 -0
  37. management/static/manifest.json +23 -0
  38. management/static/styles.css +22 -0
  39. management/tasks.py +192 -0
  40. management/telebot.py +62 -0
  41. management/templates/management/%} +1 -0
  42. management/templates/management/admin-panel-management.html +276 -0
  43. management/templates/management/admin_dashboard.html +35 -0
  44. management/templates/management/admin_login.html +112 -0
  45. management/templates/management/amin_login.html +44 -0
  46. management/templates/management/base.html +25 -0
  47. management/templates/management/create_discount_code.html +62 -0
  48. management/templates/management/create_notification.html +198 -0
  49. management/templates/management/error.html +43 -0
  50. management/templates/management/home.html +30 -0
key.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # management/generate_vapid_keys.py
2
+ # -*- coding: utf-8 -*-
3
+ import base64
4
+ import os
5
+ from cryptography.hazmat.primitives.asymmetric import ec
6
+ from cryptography.hazmat.primitives import hashes
7
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
8
+ from cryptography.hazmat.backends import default_backend
9
+
10
+ # تابعی برای تولید کلیدهای VAPID
11
+ def generate_vapid_keys():
12
+ """
13
+ این تابع یک جفت کلید عمومی و خصوصی VAPID تولید می‌کند.
14
+ """
15
+ private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
16
+ public_key = private_key.public_key()
17
+
18
+ # برای استفاده در فرانت‌اند
19
+ public_key_bytes = public_key.public_numbers().x.to_bytes(32, 'big') + public_key.public_numbers().y.to_bytes(32, 'big')
20
+ public_key_b64 = base64.urlsafe_b64encode(public_key_bytes).decode('utf-8').rstrip('=')
21
+
22
+ # برای استفاده در بک‌اند
23
+ private_key_bytes = private_key.private_numbers().private_value.to_bytes(32, 'big')
24
+ private_key_b64 = base64.urlsafe_b64encode(private_key_bytes).decode('utf-8').rstrip('=')
25
+
26
+ print("VAPID Public Key:", public_key_b64)
27
+ print("VAPID Private Key:", private_key_b64)
28
+
29
+ if __name__ == '__main__':
30
+ generate_vapid_keys()
31
+
32
+
manage.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Django's command-line utility for administrative tasks."""
3
+ import os
4
+ import sys
5
+
6
+ def main():
7
+ """Run administrative tasks."""
8
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'my_panel_project.settings')
9
+ try:
10
+ from django.core.management import execute_from_command_line
11
+ except ImportError as exc:
12
+ raise ImportError(
13
+ "Couldn't import Django. Are you sure it's installed and "
14
+ "available on your PYTHONPATH environment variable? Did you "
15
+ "forget to activate a virtual environment?"
16
+ ) from exc
17
+ execute_from_command_line(sys.argv)
18
+
19
+ if __name__ == '__main__':
20
+ main()
management/__init__.py ADDED
File without changes
management/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (142 Bytes). View file
 
management/__pycache__/admin.cpython-312.pyc ADDED
Binary file (5.95 kB). View file
 
management/__pycache__/apps.cpython-312.pyc ADDED
Binary file (456 Bytes). View file
 
management/__pycache__/backends.cpython-312.pyc ADDED
Binary file (4.96 kB). View file
 
management/__pycache__/forms.cpython-312.pyc ADDED
Binary file (6.34 kB). View file
 
management/__pycache__/marzban_api.cpython-312.pyc ADDED
Binary file (4.2 kB). View file
 
management/__pycache__/marzban_client.cpython-312.pyc ADDED
Binary file (5.03 kB). View file
 
management/__pycache__/models.cpython-312.pyc ADDED
Binary file (17.3 kB). View file
 
management/__pycache__/serializers.cpython-312.pyc ADDED
Binary file (12.7 kB). View file
 
management/__pycache__/tasks.cpython-312.pyc ADDED
Binary file (9.22 kB). View file
 
management/__pycache__/urls.cpython-312.pyc ADDED
Binary file (3.55 kB). View file
 
management/__pycache__/utils.cpython-312.pyc ADDED
Binary file (382 Bytes). View file
 
management/__pycache__/views.cpython-312.pyc ADDED
Binary file (22.7 kB). View file
 
management/admin.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # management/admin.py
2
+ # -*- coding: utf-8 -*-
3
+ from django.contrib import admin
4
+ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
5
+ from .models import (
6
+ CustomUser,
7
+ AdminLevel2,
8
+ Panel,
9
+ License,
10
+ EndUser,
11
+ Plan,
12
+ Subscription,
13
+ Payment,
14
+ DiscountCode,
15
+ TelegramUser,
16
+ SecurityToken,
17
+ PushSubscription,
18
+ PaymentMethod,
19
+ PaymentSetting,
20
+ PaymentDetail
21
+ )
22
+
23
+ # تنظیمات ادمین برای مدل CustomUser
24
+ @admin.register(CustomUser)
25
+ class CustomUserAdmin(BaseUserAdmin):
26
+ list_display = ('username', 'email', 'role', 'is_staff')
27
+ list_filter = ('role', 'is_staff', 'is_superuser', 'is_active')
28
+ search_fields = ('username', 'email')
29
+ ordering = ('username',)
30
+ fieldsets = BaseUserAdmin.fieldsets + (
31
+ ('اطلاعات نقش', {'fields': ('role', 'marzban_admin_id')}),
32
+ )
33
+
34
+ # تنظیمات ادمین برای مدل AdminLevel2
35
+ @admin.register(AdminLevel2)
36
+ class AdminLevel2Admin(admin.ModelAdmin):
37
+ # FIXED: 'marzban_api_token' does not exist in the model
38
+ list_display = ('user', 'telegram_chat_id', 'license_expiry_date')
39
+ search_fields = ('user__username',)
40
+
41
+ # تنظیمات ادمین برای مدل Panel
42
+ @admin.register(Panel)
43
+ class PanelAdmin(admin.ModelAdmin):
44
+ # FIXED: 'url' and 'api_token' do not exist. Used correct fields.
45
+ list_display = ('name', 'owner', 'marzban_host')
46
+ search_fields = ('name', 'owner__username')
47
+
48
+ # تنظیمات ادمین برای مدل License
49
+ @admin.register(License)
50
+ class LicenseAdmin(admin.ModelAdmin):
51
+ # FIXED: Used correct field names from the model ('key', 'owner', etc.)
52
+ list_display = ('key', 'owner', 'is_active', 'created_at', 'expiry_date')
53
+ list_filter = ('is_active',)
54
+ search_fields = ('key', 'owner__username')
55
+
56
+ # تنظیمات ادمین برای مدل EndUser
57
+ @admin.register(EndUser)
58
+ class EndUserAdmin(admin.ModelAdmin):
59
+ list_display = ('username', 'panel', 'marzban_user_id')
60
+ search_fields = ('username', 'marzban_user_id')
61
+ list_filter = ('panel',)
62
+
63
+ # تنظیمات ادمین برای مدل Plan
64
+ @admin.register(Plan)
65
+ class PlanAdmin(admin.ModelAdmin):
66
+ list_display = ('name', 'panel', 'price', 'duration_days', 'data_limit_gb', 'is_active')
67
+ list_filter = ('is_active', 'panel')
68
+ search_fields = ('name',)
69
+
70
+ # تنظیمات ادمین برای مدل Subscription
71
+ @admin.register(Subscription)
72
+ class SubscriptionAdmin(admin.ModelAdmin):
73
+ list_display = ('end_user', 'plan', 'status', 'start_date', 'end_date', 'remaining_data_gb')
74
+ # FIXED: 'is_trial' does not exist in the model
75
+ list_filter = ('status', 'plan', 'panel')
76
+ search_fields = ('end_user__username',)
77
+ date_hierarchy = 'start_date'
78
+
79
+ # تنظیمات ادمین برای مدل Payment
80
+ @admin.register(Payment)
81
+ class PaymentAdmin(admin.ModelAdmin):
82
+ list_display = ('subscription', 'admin', 'amount', 'status', 'created_at')
83
+ # IMPROVED: Filtering on image/text fields is not useful. Using status instead.
84
+ list_filter = ('status', 'admin')
85
+ search_fields = ('subscription__end_user__username',)
86
+ date_hierarchy = 'created_at'
87
+
88
+ # تنظیمات ادمین برای مدل DiscountCode
89
+ @admin.register(DiscountCode)
90
+ class DiscountCodeAdmin(admin.ModelAdmin):
91
+ list_display = ('code', 'admin', 'discount_percentage', 'is_active')
92
+ list_filter = ('is_active',)
93
+ search_fields = ('code', 'admin__username',)
94
+
95
+ # تنظیمات ادمین برای مدل TelegramUser
96
+ @admin.register(TelegramUser)
97
+ class TelegramUserAdmin(admin.ModelAdmin):
98
+ list_display = ('username', 'chat_id', 'admin_id')
99
+ search_fields = ('username', 'chat_id')
100
+
101
+ # تنظیمات ادمین برای مدل SecurityToken
102
+ @admin.register(SecurityToken)
103
+ class SecurityTokenAdmin(admin.ModelAdmin):
104
+ list_display = ('admin_id', 'token', 'expiration_date')
105
+ search_fields = ('admin_id', 'token')
106
+
107
+ # تنظیمات ادمین برای مدل PushSubscription
108
+ @admin.register(PushSubscription)
109
+ class PushSubscriptionAdmin(admin.ModelAdmin):
110
+ list_display = ('user', 'created_at')
111
+ search_fields = ('user__username',)
112
+
113
+ # تنظیمات ادمین برای مدل PaymentMethod
114
+ @admin.register(PaymentMethod)
115
+ class PaymentMethodAdmin(admin.ModelAdmin):
116
+ list_display = ('name', 'is_active', 'can_be_managed_by_level3')
117
+ list_filter = ('is_active', 'can_be_managed_by_level3')
118
+
119
+ # تنظیمات ادمین برای مدل PaymentSetting
120
+ @admin.register(PaymentSetting)
121
+ class PaymentSettingAdmin(admin.ModelAdmin):
122
+ list_display = ('admin_level_3', 'payment_method', 'is_active')
123
+ list_filter = ('is_active', 'payment_method')
124
+ search_fields = ('admin_level_3__user__username',)
125
+
126
+ # تنظیمات ادمین برای مدل PaymentDetail
127
+ @admin.register(PaymentDetail)
128
+ class PaymentDetailAdmin(admin.ModelAdmin):
129
+ list_display = ('admin_level_3', 'card_number', 'wallet_address')
130
+ search_fields = ('admin_level_3__user__username',)
management/backends.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # management/backends.py
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import asyncio
5
+ import logging
6
+
7
+ from django.contrib.auth.backends import BaseBackend
8
+ from django.contrib.auth import get_user_model
9
+ from marzban_api.marzban_client import Marzban
10
+ from marzban_api.exceptions import AuthenticationError, MarzbanAPIError
11
+
12
+ from .models import MarzbanAdmin, MarzbanUser, Panel, Role
13
+
14
+ CustomUser = get_user_model()
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class CustomMarzbanBackend(BaseBackend):
18
+ """
19
+ بک‌اند احراز هویت که admin_id و اطلاعات ادمین را از دیتابیس مرزبان پیدا می‌کند.
20
+ """
21
+ def authenticate(self, request, username=None, **kwargs):
22
+ if not username:
23
+ return None
24
+
25
+ try:
26
+ # 1. پیدا کردن admin_id از دیتابیس مرزبان
27
+ marzban_user = MarzbanUser.objects.using('marzban_db').get(username=username)
28
+ admin_id = marzban_user.admin_id
29
+
30
+ if not admin_id:
31
+ logger.warning(f"User {username} has no associated admin in Marzban DB.")
32
+ return None
33
+
34
+ # 2. پیدا کردن اطلاعات ادمین از دیتابیس مرزبان
35
+ try:
36
+ admin_obj = MarzbanAdmin.objects.using('marzban_db').get(id=admin_id)
37
+ except MarzbanAdmin.DoesNotExist:
38
+ logger.error(f"Admin with ID {admin_id} not found in Marzban DB.")
39
+ return None
40
+
41
+ admin_username = admin_obj.username
42
+ admin_password = admin_obj.password
43
+
44
+ # 3. لاگین به API مرزبان با مشخصات ادمین و تایید وجود کاربر
45
+ async def authenticate_with_marzban():
46
+ # در این مرحله، آدرس پنل باید از یک مکان مشخص (مثلاً مدل Panel) خوانده شود.
47
+ # برای سادگی، فعلا یک آدرس پیش‌فرض در نظر می‌گیریم.
48
+ panel_address = 'http://your_marzban_panel_address.com'
49
+
50
+ try:
51
+ async with Marzban(admin_username, admin_password, panel_address) as marzban_client:
52
+ await marzban_client.login_admin()
53
+ await marzban_client.get_user_info(username)
54
+
55
+ # 4. ایجاد یا دریافت کاربر در دیتابیس لوکال و اختصاص نقش
56
+ user, created = CustomUser.objects.get_or_create(username=username)
57
+ if created:
58
+ user_role, _ = Role.objects.get_or_create(name=Role.USER)
59
+ user.role = user_role
60
+ user.save()
61
+ return user
62
+
63
+ except AuthenticationError as e:
64
+ logger.error(f"Marzban API authentication failed for admin {admin_username}: {e}")
65
+ return None
66
+ except MarzbanAPIError as e:
67
+ logger.error(f"User {username} not found in Marzban via admin {admin_username}: {e}")
68
+ return None
69
+ except Exception as e:
70
+ logger.error(f"An unknown error occurred during Marzban API authentication: {e}")
71
+ return None
72
+
73
+ authenticated_user = asyncio.run(authenticate_with_marzban())
74
+ if authenticated_user:
75
+ return authenticated_user
76
+
77
+ except MarzbanUser.DoesNotExist:
78
+ logger.warning(f"User {username} not found in Marzban database.")
79
+ return None
80
+ except Exception as e:
81
+ logger.error(f"An unknown error occurred: {e}")
82
+ return None
83
+
84
+ def get_user(self, user_id):
85
+ try:
86
+ return CustomUser.objects.get(pk=user_id)
87
+ except CustomUser.DoesNotExist:
88
+ return None
89
+
management/forms.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # management/forms.py
2
+ # -*- coding: utf-8 -*-
3
+ from django import forms
4
+ from .models import DiscountCode
5
+
6
+ class UserLoginForm(forms.Form):
7
+ username = forms.CharField(max_length=150, label="نام کاربری")
8
+
9
+ class DiscountCodeForm(forms.ModelForm):
10
+ class Meta:
11
+ model = DiscountCode
12
+ fields = ['code', 'discount_type', 'value', 'expiration_date', 'is_active', 'min_new_subscriptions', 'max_uses', 'max_uses_per_user']
13
+
management/manage.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Django's command-line utility for administrative tasks."""
3
+ import os
4
+ import sys
5
+
6
+ def main():
7
+ """Run administrative tasks."""
8
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'my_panel_project.settings')
9
+ try:
10
+ from django.core.management import execute_from_command_line
11
+ except ImportError as exc:
12
+ raise ImportError(
13
+ "Couldn't import Django. Are you sure it's installed and "
14
+ "available on your PYTHONPATH environment variable? Did you "
15
+ "forget to activate a virtual environment?"
16
+ ) from exc
17
+ execute_from_command_line(sys.argv)
18
+
19
+ if __name__ == '__main__':
20
+ main()
management/management/commands/backup_all_databases.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # management/commands/backup_all_databases.py
2
+ # -*- coding: utf-8 -*-
3
+ import os
4
+ import datetime
5
+ import subprocess
6
+ from django.conf import settings
7
+ from django.core.management.base import BaseCommand, CommandError
8
+ from django.db import connections
9
+ from management.models import Panel
10
+ from management.tasks import send_backup_to_telegram
11
+
12
+ class Command(BaseCommand):
13
+ help = 'Backs up all configured databases (Django and all Marzban panels) and sends them to Telegram.'
14
+
15
+ def handle(self, *args, **options):
16
+ # Define the backup directory
17
+ backup_dir = os.path.join(settings.BASE_DIR, 'backups')
18
+ os.makedirs(backup_dir, exist_ok=True)
19
+
20
+ # Create a timestamped directory for the current backup
21
+ timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
22
+ current_backup_dir = os.path.join(backup_dir, timestamp)
23
+ os.makedirs(current_backup_dir)
24
+
25
+ # Get database configurations to backup
26
+ db_configs = {
27
+ 'default': connections['default']
28
+ }
29
+
30
+ # Dynamically add all Marzban panels to the backup list
31
+ try:
32
+ for panel in Panel.objects.all():
33
+ db_key = f"marzban_panel_{panel.id}"
34
+ db_configs[db_key] = {
35
+ 'ENGINE': panel.db_engine,
36
+ 'NAME': panel.db_name,
37
+ 'USER': panel.db_user,
38
+ 'PASSWORD': panel.db_password,
39
+ 'HOST': panel.db_host,
40
+ 'PORT': panel.db_port,
41
+ 'TELEGRAM_BOT_TOKEN': panel.telegram_bot_token,
42
+ 'MARZBAN_PANEL_DOMAIN': panel.domain
43
+ }
44
+ except Exception as e:
45
+ self.stdout.write(self.style.ERROR(
46
+ f"Failed to retrieve Marzban panel information. Error: {e}"
47
+ ))
48
+ return
49
+
50
+ for db_key, db_config in db_configs.items():
51
+ if 'mysql' not in db_config.get('ENGINE', ''):
52
+ self.stdout.write(self.style.WARNING(
53
+ f"Skipping database '{db_key}' as it is not a MySQL database."
54
+ ))
55
+ continue
56
+
57
+ output_file = os.path.join(current_backup_dir, f"{db_key}_{timestamp}.sql")
58
+
59
+ mysqldump_cmd = [
60
+ 'mysqldump',
61
+ f'--host={db_config.get("HOST", "localhost")}',
62
+ f'--port={db_config.get("PORT", 3306)}',
63
+ f'--user={db_config.get("USER", "")}',
64
+ f'--password={db_config.get("PASSWORD", "")}',
65
+ db_config['NAME'],
66
+ '--result-file', output_file
67
+ ]
68
+
69
+ try:
70
+ self.stdout.write(f"Backing up database '{db_key}' to {output_file}...")
71
+ subprocess.run(mysqldump_cmd, check=True, text=True)
72
+ self.stdout.write(self.style.SUCCESS(
73
+ f"Successfully backed up database '{db_key}'."
74
+ ))
75
+
76
+ # If it's a Marzban panel, send it to Telegram
77
+ if 'marzban' in db_key:
78
+ bot_token = db_config.get('TELEGRAM_BOT_TOKEN')
79
+ if bot_token and settings.TELEGRAM_ADMIN_CHAT_ID:
80
+ message = f"✅ Backup of Marzban panel at {db_config.get('MARZBAN_PANEL_DOMAIN', 'N/A')} is ready."
81
+ send_backup_to_telegram.delay(
82
+ settings.TELEGRAM_ADMIN_CHAT_ID,
83
+ message,
84
+ output_file,
85
+ bot_token
86
+ )
87
+ self.stdout.write(self.style.SUCCESS(
88
+ f"Scheduled sending backup for '{db_key}' to Telegram."
89
+ ))
90
+ except FileNotFoundError:
91
+ raise CommandError(
92
+ "mysqldump command not found. Please ensure it is installed and in your system's PATH."
93
+ )
94
+ except subprocess.CalledProcessError as e:
95
+ raise CommandError(
96
+ f"Failed to backup database '{db_key}'. Error: {e.stderr}"
97
+ )
98
+
99
+ self.stdout.write(self.style.SUCCESS('All databases backed up successfully.'))
100
+
management/management/commands/check_payments.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.core.management.base import BaseCommand
2
+ from django.utils import timezone
3
+ from datetime import timedelta
4
+ from management.models import Payment, Subscription
5
+
6
+ class Command(BaseCommand):
7
+ help = 'Checks for pending payments older than 48 hours and deactivates subscriptions.'
8
+
9
+ def handle(self, *args, **options):
10
+ # زمان فعلی منهای 48 ساعت
11
+ forty_eight_hours_ago = timezone.now() - timedelta(hours=48)
12
+
13
+ # پیدا کردن پرداخت‌های در انتظار که بیش از 48 ساعت ازشون گذشته
14
+ overdue_payments = Payment.objects.filter(
15
+ status='pending',
16
+ created_at__lt=forty_eight_hours_ago
17
+ )
18
+
19
+ if not overdue_payments:
20
+ self.stdout.write(self.style.SUCCESS('No overdue pending payments found.'))
21
+ return
22
+
23
+ self.stdout.write(self.style.WARNING(f'Found {overdue_payments.count()} overdue payments.'))
24
+
25
+ # غیرفعال کردن اشتراک‌های مربوطه
26
+ for payment in overdue_payments:
27
+ try:
28
+ subscription = payment.subscription
29
+ if subscription and subscription.status == 'active':
30
+ subscription.status = 'inactive'
31
+ subscription.save()
32
+ payment.status = 'expired'
33
+ payment.save()
34
+ self.stdout.write(self.style.SUCCESS(
35
+ f'Successfully deactivated subscription for user: {subscription.user.username}'
36
+ ))
37
+ except Subscription.DoesNotExist:
38
+ self.stdout.write(self.style.ERROR(
39
+ f'Subscription not found for payment ID: {payment.id}'
40
+ ))
41
+
42
+ self.stdout.write(self.style.SUCCESS('Finished checking payments.'))
management/management/commands/restore_all_databases.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # management/commands/restore_all_databases.py
2
+ # -*- coding: utf-8 -*-
3
+ import os
4
+ import subprocess
5
+ from django.conf import settings
6
+ from django.core.management.base import BaseCommand, CommandError
7
+ from django.db import connections
8
+ from management.models import Panel
9
+
10
+ class Command(BaseCommand):
11
+ help = 'Restores all databases from a specified timestamped backup directory.'
12
+
13
+ def add_arguments(self, parser):
14
+ parser.add_argument(
15
+ 'timestamp',
16
+ type=str,
17
+ help='The timestamp of the backup directory to restore (e.g., 2023-10-27_10-30-00).',
18
+ )
19
+
20
+ def handle(self, *args, **options):
21
+ timestamp = options['timestamp']
22
+ backup_dir = os.path.join(settings.BASE_DIR, 'backups', timestamp)
23
+
24
+ if not os.path.isdir(backup_dir):
25
+ raise CommandError(f"Backup directory '{backup_dir}' not found.")
26
+
27
+ self.stdout.write(self.style.WARNING(
28
+ f"WARNING: This will overwrite your current databases with data from '{timestamp}'. "
29
+ f"Proceed with caution. Type 'yes' to continue."
30
+ ))
31
+ confirm = input("> ")
32
+ if confirm.lower() != 'yes':
33
+ self.stdout.write("Restore operation cancelled.")
34
+ return
35
+
36
+ # Get database configurations to restore
37
+ db_configs = {
38
+ 'default': connections['default']
39
+ }
40
+
41
+ try:
42
+ for panel in Panel.objects.all():
43
+ db_key = f"marzban_panel_{panel.id}"
44
+ db_configs[db_key] = {
45
+ 'ENGINE': panel.db_engine,
46
+ 'NAME': panel.db_name,
47
+ 'USER': panel.db_user,
48
+ 'PASSWORD': panel.db_password,
49
+ 'HOST': panel.db_host,
50
+ 'PORT': panel.db_port,
51
+ }
52
+ except Exception as e:
53
+ self.stdout.write(self.style.ERROR(
54
+ f"Failed to retrieve Marzban panel information. Error: {e}"
55
+ ))
56
+ return
57
+
58
+
59
+ for db_key, db_config in db_configs.items():
60
+ if 'mysql' not in db_config.get('ENGINE', ''):
61
+ self.stdout.write(self.style.WARNING(
62
+ f"Skipping restore for database '{db_key}' as it is not a MySQL database."
63
+ ))
64
+ continue
65
+
66
+ sql_file = os.path.join(backup_dir, f"{db_key}_{timestamp}.sql")
67
+
68
+ if not os.path.isfile(sql_file):
69
+ self.stdout.write(self.style.WARNING(
70
+ f"Skipping restore for database '{db_key}': backup file not found at {sql_file}."
71
+ ))
72
+ continue
73
+
74
+ mysql_cmd = [
75
+ 'mysql',
76
+ f'--host={db_config.get("HOST", "localhost")}',
77
+ f'--port={db_config.get("PORT", 3306)}',
78
+ f'--user={db_config.get("USER", "")}',
79
+ f'--password={db_config.get("PASSWORD", "")}',
80
+ db_config['NAME']
81
+ ]
82
+
83
+ try:
84
+ self.stdout.write(f"Restoring database '{db_key}' from {sql_file}...")
85
+ with open(sql_file, 'r') as f:
86
+ subprocess.run(mysql_cmd, stdin=f, check=True, text=True)
87
+ self.stdout.write(self.style.SUCCESS(
88
+ f"Successfully restored database '{db_key}'."
89
+ ))
90
+ except FileNotFoundError:
91
+ raise CommandError(
92
+ "mysql command not found. Please ensure it is installed and in your system's PATH."
93
+ )
94
+ except subprocess.CalledProcessError as e:
95
+ raise CommandError(
96
+ f"Failed to restore database '{db_key}'. Error: {e.stderr}"
97
+ )
98
+
99
+ self.stdout.write(self.style.SUCCESS('All databases restored successfully.'))
100
+
management/management/commands/schedule_reminders.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # management/management/commands/schedule_reminders.py
2
+
3
+ # -*- coding: utf-8 -*-
4
+
5
+ import os
6
+
7
+ import uuid
8
+
9
+ import datetime
10
+
11
+ from django.core.management.base import BaseCommand
12
+
13
+ from django.conf import settings
14
+
15
+ from django.db import connections
16
+
17
+ from management.models import Panel
18
+
19
+ from management.tasks import send_telegram_notification
20
+
21
+
22
+
23
+ # Dynamically import the unmanaged models
24
+
25
+ # This will be used to access the Marzban database tables
26
+
27
+ try:
28
+
29
+     from management.models import MarzbanUser, TelegramUser
30
+
31
+ except ImportError:
32
+
33
+     # Handle the case where the models might not be fully defined yet
34
+
35
+     pass
36
+
37
+
38
+
39
+ class Command(BaseCommand):
40
+
41
+     help = 'Schedules renewal and usage reminders for Marzban users.'
42
+
43
+
44
+
45
+     def handle(self, *args, **options):
46
+
47
+         # Retrieve the bot token from the environment variables or settings
48
+
49
+         # IMPORTANT: You need to define this in your settings or as an environment variable
50
+
51
+         telegram_bot_token = os.environ.get('TELEGRAM_BOT_TOKEN', 'YOUR_DEFAULT_BOT_TOKEN_HERE')
52
+
53
+
54
+
55
+         if not telegram_bot_token:
56
+
57
+             self.stdout.write(self.style.ERROR('Telegram bot token is not configured. Aborting.'))
58
+
59
+             return
60
+
61
+
62
+
63
+         self.stdout.write(self.style.SUCCESS('Starting reminder scheduling for all users...'))
64
+
65
+
66
+
67
+         # Get all panels from the main database
68
+
69
+         panels = Panel.objects.all()
70
+
71
+
72
+
73
+         for panel in panels:
74
+
75
+             self.stdout.write(f'Processing panel: {panel.name} (ID: {panel.id})')
76
+
77
+             
78
+
79
+             # Dynamically set up the database connection for the current panel
80
+
81
+             marzban_db_settings = {
82
+
83
+                 'ENGINE': 'django.db.backends.mysql',
84
+
85
+                 'NAME': panel.db_name,
86
+
87
+                 'USER': panel.db_user,
88
+
89
+                 'PASSWORD': panel.db_password,
90
+
91
+                 'HOST': panel.db_host,
92
+
93
+                 'PORT': panel.db_port,
94
+
95
+             }
96
+
97
+             settings.DATABASES['marzban_db'] = marzban_db_settings
98
+
99
+
100
+
101
+             try:
102
+
103
+                 # Use the dynamic connection to query the MarzbanUser and TelegramUser models
104
+
105
+                 with connections['marzban_db'].cursor() as cursor:
106
+
107
+                     # Get all users from the Marzban database
108
+
109
+                     users_to_check = MarzbanUser.objects.using('marzban_db').all()
110
+
111
+
112
+
113
+                     for user in users_to_check:
114
+
115
+                         # --- Here is the core logic for reminders ---
116
+
117
+                         
118
+
119
+                         # Find the Telegram chat_id for this user
120
+
121
+                         try:
122
+
123
+                             # We assume the TelegramUser model is now linked to the user's ID
124
+
125
+                             telegram_user = TelegramUser.objects.using('marzban_db').get(user_id=user.pk)
126
+
127
+                             chat_id = telegram_user.chat_id
128
+
129
+                         except TelegramUser.DoesNotExist:
130
+
131
+                             self.stdout.write(f'  No Telegram chat_id found for user {user.username}. Skipping.')
132
+
133
+                             continue
134
+
135
+
136
+
137
+                         # Check for expiration
138
+
139
+                         is_expiring = False
140
+
141
+                         if user.expire_date:
142
+
143
+                             time_left = user.expire_date - datetime.datetime.now(user.expire_date.tzinfo)
144
+
145
+                             if time_left < datetime.timedelta(days=3): # Expiring in less than 3 days
146
+
147
+                                 is_expiring = True
148
+
149
+                                 message = (
150
+
151
+                                     f"⚠️ سلام {user.username}، سرویس شما در تاریخ {user.expire_date.strftime('%Y-%m-%d')} منقضی می‌شود.\n"
152
+
153
+                                     "لطفاً برای تمدید سرویس به پنل کاربری خود مراجعه کنید."
154
+
155
+                                 )
156
+
157
+                                 send_telegram_notification.delay(chat_id, message, telegram_bot_token)
158
+
159
+                                 self.stdout.write(f'  Scheduled expiration reminder for user {user.username}.')
160
+
161
+
162
+
163
+                         # Check for usage limit (e.g., 80% of total)
164
+
165
+                         is_over_usage_limit = False
166
+
167
+                         if user.data_limit and user.data_usage:
168
+
169
+                             usage_percentage = (user.data_usage / user.data_limit) * 100
170
+
171
+                             if usage_percentage >= 80:
172
+
173
+ ��                               is_over_usage_limit = True
174
+
175
+                                 message = (
176
+
177
+                                     f"⚠️ سلام {user.username}، حجم باقی‌مانده سرویس شما کم است.\n"
178
+
179
+                                     f"میزان مصرف: {usage_percentage:.2f}%\n"
180
+
181
+                                     "لطفاً برای خرید حجم جدید به پنل کاربری خود مراجعه کنید."
182
+
183
+                                 )
184
+
185
+                                 # Avoid sending duplicate messages if both conditions are met
186
+
187
+                                 if not is_expiring:
188
+
189
+                                     send_telegram_notification.delay(chat_id, message, telegram_bot_token)
190
+
191
+                                     self.stdout.write(f'  Scheduled usage reminder for user {user.username}.')
192
+
193
+                                 
194
+
195
+             except Exception as e:
196
+
197
+                 self.stdout.write(self.style.ERROR(f'An error occurred while processing panel {panel.name}: {e}'))
198
+
199
+
200
+
201
+         self.stdout.write(self.style.SUCCESS('Reminder scheduling finished.'))
202
+
203
+
management/marzban_client.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp
2
+ import json
3
+ import asyncio
4
+
5
+ # --- کد از فایل send_requests.py ---
6
+ async def send_request(endpoint, token, method, data=None):
7
+ panel_address = token["panel_address"]
8
+ token_type = token["token_type"]
9
+ access_token = token["access_token"]
10
+ request_address = f"{panel_address}/api/{endpoint}"
11
+ headers = {
12
+ "Content-Type": "application/json",
13
+ "Authorization": f"{token_type} {access_token}",
14
+ }
15
+ async with aiohttp.request(
16
+ method=method,
17
+ url=request_address,
18
+ headers=headers,
19
+ data=json.dumps(data) if data else None,
20
+ raise_for_status=True
21
+ ) as response:
22
+ result = await response.json()
23
+ return result
24
+
25
+ # --- کد از فایل user.py ---
26
+ class User:
27
+ def __init__(self, username, **kwargs):
28
+ self.username = username
29
+ self.proxies = kwargs.get('proxies', {})
30
+ self.inbounds = kwargs.get('inbounds', {})
31
+ self.data_limit = kwargs.get('data_limit', 0)
32
+ self.data_limit_reset_strategy = kwargs.get('data_limit_reset_strategy', "no_reset")
33
+ self.status = kwargs.get('status', "")
34
+ self.expire = kwargs.get('expire', 0)
35
+ self.used_traffic = kwargs.get('used_traffic', 0)
36
+ self.lifetime_used_traffic = kwargs.get('lifetime_used_traffic', 0)
37
+ self.created_at = kwargs.get('created_at', "")
38
+ self.links = kwargs.get('links', [])
39
+ self.subscription_url = kwargs.get('subscription_url', "")
40
+ self.excluded_inbounds = kwargs.get('excluded_inbounds', {})
41
+ self.full_name = kwargs.get('full_name', "")
42
+ class UserMethods:
43
+ async def get_all_users(self, token: dict, username=None, status=None):
44
+ endpoint = "users"
45
+ if username:
46
+ endpoint += f"?username={username}"
47
+ if status:
48
+ if "?" in endpoint:
49
+ endpoint += f"&status={status}"
50
+ else:
51
+ endpoint += f"?status={status}"
52
+ request = await send_request(endpoint, token, "get")
53
+ user_list = [User(**user) for user in request["users"]]
54
+ return user_list
55
+
56
+ # --- کد از فایل admin.py ---
57
+ class Admin:
58
+ def __init__(self, username: str, password: str, panel_address: str):
59
+ self.username = username
60
+ self.password = password
61
+ self.panel_address = panel_address
62
+
63
+ async def get_token(self):
64
+ try:
65
+ async with aiohttp.request(
66
+ "post",
67
+ url=f"{self.panel_address}/api/admin/token",
68
+ data={"username": self.username, "password": self.password},
69
+ raise_for_status=True
70
+ ) as response:
71
+ result = await response.json()
72
+ result["panel_address"] = self.panel_address
73
+ return result
74
+ except aiohttp.exceptions.RequestException as ex:
75
+ print(f"Request Exception: {ex}")
76
+ return None
77
+ except json.JSONDecodeError as ex:
78
+ print(f"JSON Decode Error: {ex}")
79
+ return None
management/migrations/0001_initial.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.2.5 on 2025-08-15 09:56
2
+
3
+ import django.contrib.auth.models
4
+ import django.contrib.auth.validators
5
+ import django.db.models.deletion
6
+ import django.utils.timezone
7
+ import uuid
8
+ from django.conf import settings
9
+ from django.db import migrations, models
10
+
11
+
12
+ class Migration(migrations.Migration):
13
+
14
+ initial = True
15
+
16
+ dependencies = [
17
+ ('auth', '0012_alter_user_first_name_max_length'),
18
+ ]
19
+
20
+ operations = [
21
+ migrations.CreateModel(
22
+ name='SecurityToken',
23
+ fields=[
24
+ ('admin_id', models.CharField(max_length=255, primary_key=True, serialize=False)),
25
+ ('token', models.CharField(default=uuid.uuid4, max_length=255, unique=True)),
26
+ ('expiration_date', models.DateTimeField()),
27
+ ],
28
+ options={
29
+ 'db_table': 'telegram_tokens',
30
+ 'managed': False,
31
+ },
32
+ ),
33
+ migrations.CreateModel(
34
+ name='TelegramUser',
35
+ fields=[
36
+ ('admin_id', models.CharField(max_length=255, primary_key=True, serialize=False, unique=True)),
37
+ ('chat_id', models.CharField(max_length=255, unique=True)),
38
+ ('username', models.CharField(max_length=255)),
39
+ ],
40
+ options={
41
+ 'db_table': 'telegram_users',
42
+ 'managed': False,
43
+ },
44
+ ),
45
+ migrations.CreateModel(
46
+ name='MarzbanUser',
47
+ fields=[
48
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
49
+ ('username', models.CharField(max_length=255, unique=True)),
50
+ ],
51
+ ),
52
+ migrations.CreateModel(
53
+ name='PaymentMethod',
54
+ fields=[
55
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
56
+ ('name', models.CharField(choices=[('gateway', 'Bank Gateway'), ('crypto', 'Cryptocurrency'), ('manual', 'Manual (Card-to-Card)')], max_length=50, unique=True, verbose_name='Payment Method Name')),
57
+ ('is_active', models.BooleanField(default=True, verbose_name='Is Active?')),
58
+ ('can_be_managed_by_level3', models.BooleanField(default=False, verbose_name='Can be managed by Admin Level 3?')),
59
+ ],
60
+ options={
61
+ 'verbose_name': 'Payment Method',
62
+ 'verbose_name_plural': 'Payment Methods',
63
+ },
64
+ ),
65
+ migrations.CreateModel(
66
+ name='CustomUser',
67
+ fields=[
68
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
69
+ ('password', models.CharField(max_length=128, verbose_name='password')),
70
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
71
+ ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
72
+ ('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')),
73
+ ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
74
+ ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
75
+ ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
76
+ ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
77
+ ('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')),
78
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
79
+ ('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)),
80
+ ('marzban_admin_id', models.UUIDField(blank=True, null=True)),
81
+ ('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')),
82
+ ('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')),
83
+ ],
84
+ options={
85
+ 'verbose_name': 'user',
86
+ 'verbose_name_plural': 'users',
87
+ 'abstract': False,
88
+ },
89
+ managers=[
90
+ ('objects', django.contrib.auth.models.UserManager()),
91
+ ],
92
+ ),
93
+ migrations.CreateModel(
94
+ name='AdminLevel2',
95
+ fields=[
96
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
97
+ ('telegram_chat_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='تلگرام چت آیدی')),
98
+ ('license_expiry_date', models.DateField(blank=True, null=True, verbose_name='تاریخ انقضای لایسنس')),
99
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='admin_level_2', to=settings.AUTH_USER_MODEL)),
100
+ ],
101
+ ),
102
+ migrations.CreateModel(
103
+ name='AdminLevel3',
104
+ fields=[
105
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
106
+ ('telegram_chat_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='تلگرام چت آیدی')),
107
+ ('parent_admin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='child_admins', to='management.adminlevel2', verbose_name='ادمین والد (سطح ۲)')),
108
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='admin_level_3', to=settings.AUTH_USER_MODEL)),
109
+ ],
110
+ ),
111
+ migrations.CreateModel(
112
+ name='DiscountCode',
113
+ fields=[
114
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
115
+ ('code', models.CharField(max_length=50, unique=True)),
116
+ ('discount_percentage', models.DecimalField(decimal_places=2, max_digits=5)),
117
+ ('is_active', models.BooleanField(default=True)),
118
+ ('admin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
119
+ ],
120
+ ),
121
+ migrations.CreateModel(
122
+ name='License',
123
+ fields=[
124
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
125
+ ('key', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='کلید لایسنس')),
126
+ ('is_active', models.BooleanField(default=True, verbose_name='وضعیت فعال')),
127
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
128
+ ('expiry_date', models.DateField(verbose_name='تاریخ انقضا')),
129
+ ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='صاحب لایسنس')),
130
+ ],
131
+ ),
132
+ migrations.CreateModel(
133
+ name='Panel',
134
+ fields=[
135
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
136
+ ('name', models.CharField(max_length=255, verbose_name='نام پنل')),
137
+ ('marzban_host', models.CharField(max_length=255, verbose_name='آدرس هاست Marzban')),
138
+ ('marzban_username', models.CharField(max_length=255, verbose_name='نام کاربری Marzban')),
139
+ ('marzban_password', models.CharField(max_length=255, verbose_name='رمز عبور Marzban')),
140
+ ('telegram_bot_token', models.CharField(blank=True, max_length=255, null=True)),
141
+ ('license', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='linked_panel', to='management.license')),
142
+ ('owner', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='owned_panel', to=settings.AUTH_USER_MODEL)),
143
+ ],
144
+ ),
145
+ migrations.CreateModel(
146
+ name='MarzbanAdmin',
147
+ fields=[
148
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
149
+ ('username', models.CharField(max_length=255, unique=True)),
150
+ ('password', models.CharField(max_length=255)),
151
+ ('permission', models.CharField(max_length=50)),
152
+ ('panel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='marzban_admins', to='management.panel')),
153
+ ],
154
+ ),
155
+ migrations.CreateModel(
156
+ name='EndUser',
157
+ fields=[
158
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
159
+ ('username', models.CharField(max_length=255, unique=True)),
160
+ ('marzban_user_id', models.CharField(blank=True, max_length=255, null=True, unique=True)),
161
+ ('telegram_chat_id', models.CharField(blank=True, max_length=255, null=True)),
162
+ ('panel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='management.panel')),
163
+ ],
164
+ ),
165
+ migrations.CreateModel(
166
+ name='PaymentDetail',
167
+ fields=[
168
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
169
+ ('card_number', models.CharField(blank=True, max_length=50, null=True)),
170
+ ('card_holder_name', models.CharField(blank=True, max_length=255, null=True)),
171
+ ('wallet_address', models.CharField(blank=True, max_length=255, null=True)),
172
+ ('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)),
173
+ ],
174
+ options={
175
+ 'verbose_name': 'Payment Detail',
176
+ 'verbose_name_plural': 'Payment Details',
177
+ },
178
+ ),
179
+ migrations.CreateModel(
180
+ name='Plan',
181
+ fields=[
182
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
183
+ ('name', models.CharField(max_length=100)),
184
+ ('price', models.DecimalField(decimal_places=2, max_digits=10)),
185
+ ('duration_days', models.IntegerField()),
186
+ ('data_limit_gb', models.FloatField()),
187
+ ('is_active', models.BooleanField(default=True)),
188
+ ('panel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plans', to='management.panel')),
189
+ ],
190
+ ),
191
+ migrations.CreateModel(
192
+ name='PushSubscription',
193
+ fields=[
194
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
195
+ ('subscription_info', models.JSONField()),
196
+ ('created_at', models.DateTimeField(auto_now_add=True)),
197
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='push_subscriptions', to=settings.AUTH_USER_MODEL)),
198
+ ],
199
+ ),
200
+ migrations.CreateModel(
201
+ name='Subscription',
202
+ fields=[
203
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
204
+ ('status', models.CharField(choices=[('active', 'فعال'), ('expired', 'منقضی شده'), ('pending', 'در انتظار پرداخت'), ('deactivated', 'غیرفعال')], default='pending', max_length=20)),
205
+ ('start_date', models.DateField(blank=True, null=True)),
206
+ ('end_date', models.DateField(blank=True, null=True)),
207
+ ('remaining_data_gb', models.FloatField(blank=True, null=True)),
208
+ ('end_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='management.enduser')),
209
+ ('panel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='management.panel')),
210
+ ('plan', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='management.plan')),
211
+ ],
212
+ ),
213
+ migrations.CreateModel(
214
+ name='Payment',
215
+ fields=[
216
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
217
+ ('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='مبلغ')),
218
+ ('status', models.CharField(choices=[('pending', 'در انتظار تأیید'), ('approved', 'تأیید شده'), ('rejected', 'رد شده')], default='pending', max_length=20, verbose_name='وضعیت')),
219
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
220
+ ('payment_token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
221
+ ('receipt_image', models.ImageField(blank=True, null=True, upload_to='receipts/', verbose_name='تصویر رسید')),
222
+ ('receipt_text', models.TextField(blank=True, null=True, verbose_name='متن رسید')),
223
+ ('admin', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='handled_payments', to=settings.AUTH_USER_MODEL)),
224
+ ('subscription', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='management.subscription')),
225
+ ],
226
+ ),
227
+ migrations.CreateModel(
228
+ name='PaymentSetting',
229
+ fields=[
230
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
231
+ ('is_active', models.BooleanField(default=False, verbose_name='Is Active for this Admin?')),
232
+ ('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)),
233
+ ('payment_method', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='management.paymentmethod')),
234
+ ],
235
+ options={
236
+ 'verbose_name': 'Payment Setting',
237
+ 'verbose_name_plural': 'Payment Settings',
238
+ 'unique_together': {('admin_level_3', 'payment_method')},
239
+ },
240
+ ),
241
+ ]
management/migrations/__init__.py ADDED
File without changes
management/migrations/__pycache__/0001_initial.cpython-312.pyc ADDED
Binary file (16.9 kB). View file
 
management/migrations/__pycache__/0002_remove_user_admin_remove_payment_user_and_more.cpython-312.pyc ADDED
Binary file (6.05 kB). View file
 
management/migrations/__pycache__/0003_notification_remove_payment_user_config_and_more.cpython-312.pyc ADDED
Binary file (12.7 kB). View file
 
management/migrations/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (153 Bytes). View file
 
management/models.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # management/models.py
2
+ # -*- coding: utf-8 -*-
3
+ from django.db import models
4
+ from django.contrib.auth.models import AbstractUser
5
+ from django.utils import timezone
6
+ import uuid
7
+ import datetime
8
+
9
+ # --- Users & Authentication ---
10
+ class CustomUser(AbstractUser):
11
+ ROLE_CHOICES = [
12
+ ('SuperAdmin', 'Super Admin'),
13
+ ('PanelOwner', 'Panel Owner'),
14
+ ('AdminLevel2', 'Admin Level 2'),
15
+ ('AdminLevel3', 'Admin Level 3'),
16
+ ('EndUser', 'End User'),
17
+ ]
18
+ role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='EndUser')
19
+ marzban_admin_id = models.UUIDField(null=True, blank=True)
20
+
21
+ def is_super_admin(self):
22
+ return self.role == 'SuperAdmin'
23
+
24
+ def is_admin_level_2(self):
25
+ return self.role in ('SuperAdmin', 'PanelOwner', 'AdminLevel2')
26
+
27
+ def is_admin_level_3(self):
28
+ return self.role in ('SuperAdmin', 'PanelOwner', 'AdminLevel2', 'AdminLevel3')
29
+
30
+ def __str__(self):
31
+ return self.username
32
+
33
+ # --- Main Website Models ---
34
+ class AdminLevel2(models.Model):
35
+ user = models.OneToOneField(
36
+ CustomUser, on_delete=models.CASCADE, related_name='admin_level_2'
37
+ )
38
+ telegram_chat_id = models.CharField(
39
+ max_length=255, blank=True, null=True,
40
+ verbose_name="تلگرام چت آیدی"
41
+ )
42
+ license_expiry_date = models.DateField(
43
+ null=True, blank=True, verbose_name="تاریخ انقضای لایسنس"
44
+ )
45
+ def __str__(self):
46
+ return f"Admin Level 2: {self.user.username}"
47
+
48
+
49
+ class AdminLevel3(models.Model):
50
+ user = models.OneToOneField(
51
+ CustomUser, on_delete=models.CASCADE, related_name='admin_level_3'
52
+ )
53
+ parent_admin = models.ForeignKey(
54
+ AdminLevel2,
55
+ on_delete=models.CASCADE,
56
+ related_name='child_admins',
57
+ verbose_name="ادمین والد (سطح ۲)"
58
+ )
59
+ telegram_chat_id = models.CharField(
60
+ max_length=255, blank=True, null=True,
61
+ verbose_name="تلگرام چت آیدی"
62
+ )
63
+ def __str__(self):
64
+ return f"Admin Level 3: {self.user.username}"
65
+
66
+ class Panel(models.Model):
67
+ owner = models.OneToOneField(CustomUser, on_delete=models.CASCADE, related_name='owned_panel')
68
+ name = models.CharField(max_length=255, verbose_name="نام پنل")
69
+ marzban_host = models.CharField(max_length=255, verbose_name="آدرس هاست Marzban")
70
+ marzban_username = models.CharField(max_length=255, verbose_name="نام کاربری Marzban")
71
+ marzban_password = models.CharField(max_length=255, verbose_name="رمز عبور Marzban")
72
+ telegram_bot_token = models.CharField(max_length=255, blank=True, null=True)
73
+ # ADDED: Link to the license
74
+ license = models.OneToOneField('License', on_delete=models.SET_NULL, null=True, blank=True, related_name='linked_panel')
75
+
76
+ def __str__(self):
77
+ return self.name
78
+
79
+ class License(models.Model):
80
+ key = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name="کلید لایسنس")
81
+ is_active = models.BooleanField(default=True, verbose_name="وضعیت فعال")
82
+ owner = models.ForeignKey(CustomUser, on_delete=models.CASCADE, verbose_name="صاحب لایسنس")
83
+ created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ ایجاد")
84
+ expiry_date = models.DateField(verbose_name="تاریخ انقضا")
85
+
86
+ def __str__(self):
87
+ return str(self.key)
88
+
89
+ # ADDED: MarzbanAdmin model (was missing)
90
+ class MarzbanAdmin(models.Model):
91
+ username = models.CharField(max_length=255, unique=True)
92
+ password = models.CharField(max_length=255)
93
+ permission = models.CharField(max_length=50)
94
+ panel = models.ForeignKey(Panel, on_delete=models.CASCADE, related_name='marzban_admins')
95
+
96
+ def __str__(self):
97
+ return self.username
98
+
99
+ # ADDED: MarzbanUser model (was missing)
100
+ class MarzbanUser(models.Model):
101
+ username = models.CharField(max_length=255, unique=True)
102
+ # Add other fields relevant to Marzban users
103
+ # ...
104
+
105
+ def __str__(self):
106
+ return self.username
107
+
108
+ class EndUser(models.Model):
109
+ username = models.CharField(max_length=255, unique=True)
110
+ panel = models.ForeignKey(Panel, on_delete=models.CASCADE)
111
+ # marzban_user_id can be nullable if the user is created first in our DB
112
+ marzban_user_id = models.CharField(max_length=255, unique=True, null=True, blank=True)
113
+ telegram_chat_id = models.CharField(max_length=255, blank=True, null=True)
114
+
115
+ def __str__(self):
116
+ return self.username
117
+
118
+ # ADDED: Plan model (was missing)
119
+ class Plan(models.Model):
120
+ name = models.CharField(max_length=100)
121
+ price = models.DecimalField(max_digits=10, decimal_places=2)
122
+ duration_days = models.IntegerField()
123
+ data_limit_gb = models.FloatField()
124
+ is_active = models.BooleanField(default=True)
125
+ panel = models.ForeignKey(Panel, on_delete=models.CASCADE, related_name='plans')
126
+
127
+ def __str__(self):
128
+ return f"{self.name} ({self.panel.name})"
129
+
130
+ # ADDED: Subscription model (was missing)
131
+ class Subscription(models.Model):
132
+ STATUS_CHOICES = [
133
+ ('active', 'فعال'),
134
+ ('expired', 'منقضی شده'),
135
+ ('pending', 'در انتظار پرداخت'),
136
+ ('deactivated', 'غیرفعال'),
137
+ ]
138
+ end_user = models.ForeignKey(EndUser, on_delete=models.CASCADE, related_name='subscriptions')
139
+ plan = models.ForeignKey(Plan, on_delete=models.PROTECT) # Don't delete plan if subscription exists
140
+ panel = models.ForeignKey(Panel, on_delete=models.CASCADE, related_name='subscriptions')
141
+ status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
142
+ start_date = models.DateField(null=True, blank=True)
143
+ end_date = models.DateField(null=True, blank=True)
144
+ remaining_data_gb = models.FloatField(null=True, blank=True)
145
+
146
+ def __str__(self):
147
+ return f"Subscription for {self.end_user.username} on plan {self.plan.name}"
148
+
149
+ # FIXED: Complete overhaul of the Payment model
150
+ class Payment(models.Model):
151
+ STATUS_CHOICES = [
152
+ ('pending', 'در انتظار تأیید'),
153
+ ('approved', 'تأیید شده'),
154
+ ('rejected', 'رد شده'),
155
+ ]
156
+ subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE, related_name='payments')
157
+ admin = models.ForeignKey(CustomUser, on_delete=models.PROTECT, related_name='handled_payments') # The admin to approve
158
+ amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="مبلغ")
159
+ status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="وضعیت")
160
+ created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ ایجاد")
161
+
162
+ # Fields for receipt upload
163
+ payment_token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
164
+ receipt_image = models.ImageField(upload_to='receipts/', blank=True, null=True, verbose_name="تصویر رسید")
165
+ receipt_text = models.TextField(blank=True, null=True, verbose_name="متن رسید")
166
+
167
+ def __str__(self):
168
+ return f"Payment for {self.subscription.end_user.username} - {self.amount} - {self.status}"
169
+
170
+
171
+ class DiscountCode(models.Model):
172
+ code = models.CharField(max_length=50, unique=True)
173
+ admin = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
174
+ discount_percentage = models.DecimalField(max_digits=5, decimal_places=2)
175
+ is_active = models.BooleanField(default=True)
176
+ def __str__(self):
177
+ return self.code
178
+
179
+ # --- Telegram Integration ---
180
+ class TelegramUser(models.Model):
181
+ admin_id = models.CharField(max_length=255, unique=True, primary_key=True)
182
+ chat_id = models.CharField(max_length=255, unique=True)
183
+ username = models.CharField(max_length=255)
184
+ class Meta:
185
+ managed = False
186
+ db_table = 'telegram_users'
187
+ def __str__(self):
188
+ return self.username
189
+
190
+ class SecurityToken(models.Model):
191
+ admin_id = models.CharField(max_length=255, primary_key=True)
192
+ token = models.CharField(max_length=255, unique=True, default=uuid.uuid4)
193
+ expiration_date = models.DateTimeField()
194
+ class Meta:
195
+ managed = False
196
+ db_table = 'telegram_tokens'
197
+ def __str__(self):
198
+ return f"Token for {self.admin_id}"
199
+
200
+ # --- Push Notifications ---
201
+ class PushSubscription(models.Model):
202
+ user = models.ForeignKey(
203
+ CustomUser, on_delete=models.CASCADE, related_name='push_subscriptions'
204
+ )
205
+ subscription_info = models.JSONField()
206
+ created_at = models.DateTimeField(auto_now_add=True)
207
+
208
+ # --- Payment System Models ---
209
+ class PaymentMethod(models.Model):
210
+ METHOD_CHOICES = [
211
+ ('gateway', 'Bank Gateway'),
212
+ ('crypto', 'Cryptocurrency'),
213
+ ('manual', 'Manual (Card-to-Card)'),
214
+ ]
215
+ name = models.CharField(max_length=50, choices=METHOD_CHOICES, unique=True, verbose_name="Payment Method Name")
216
+ is_active = models.BooleanField(default=True, verbose_name="Is Active?")
217
+ can_be_managed_by_level3 = models.BooleanField(default=False, verbose_name="Can be managed by Admin Level 3?")
218
+
219
+ class Meta:
220
+ verbose_name = "Payment Method"
221
+ verbose_name_plural = "Payment Methods"
222
+
223
+ def __str__(self):
224
+ return self.get_name_display()
225
+
226
+ class PaymentSetting(models.Model):
227
+ admin_level_3 = models.ForeignKey(
228
+ CustomUser,
229
+ on_delete=models.CASCADE,
230
+ limit_choices_to={'role': 'AdminLevel3'},
231
+ related_name='payment_settings'
232
+ )
233
+ payment_method = models.ForeignKey(PaymentMethod, on_delete=models.CASCADE)
234
+ is_active = models.BooleanField(default=False, verbose_name="Is Active for this Admin?")
235
+
236
+ class Meta:
237
+ unique_together = ('admin_level_3', 'payment_method')
238
+ verbose_name = "Payment Setting"
239
+ verbose_name_plural = "Payment Settings"
240
+
241
+ def __str__(self):
242
+ return f"{self.admin_level_3.username}'s {self.payment_method.get_name_display()} Setting"
243
+
244
+ class PaymentDetail(models.Model):
245
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
246
+ admin_level_3 = models.OneToOneField(
247
+ CustomUser,
248
+ on_delete=models.CASCADE,
249
+ limit_choices_to={'role': 'AdminLevel3'},
250
+ related_name='payment_details'
251
+ )
252
+ card_number = models.CharField(max_length=50, blank=True, null=True)
253
+ card_holder_name = models.CharField(max_length=255, blank=True, null=True)
254
+ wallet_address = models.CharField(max_length=255, blank=True, null=True)
255
+
256
+ class Meta:
257
+ verbose_name = "Payment Detail"
258
+ verbose_name_plural = "Payment Details"
259
+
260
+ def __str__(self):
261
+ return f"Payment details for {self.admin_level_3.username}"
management/serializers.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # management/serializers.py
2
+ # -*- coding: utf-8 -*-
3
+ from rest_framework import serializers
4
+ from django.utils import timezone
5
+ import datetime
6
+ from .models import (
7
+ CustomUser,
8
+ Panel,
9
+ License,
10
+ PushSubscription,
11
+ MarzbanAdmin,
12
+ SecurityToken,
13
+ DiscountCode,
14
+ Plan,
15
+ Subscription,
16
+ Payment,
17
+ EndUser,
18
+ PaymentMethod,
19
+ PaymentSetting,
20
+ PaymentDetail,
21
+ )
22
+
23
+ # --- Serializers for Django Rest Framework ---
24
+
25
+ # ADDED: LicenseSerializer (was missing)
26
+ class LicenseSerializer(serializers.ModelSerializer):
27
+ """Serializer for the License model."""
28
+ class Meta:
29
+ model = License
30
+ fields = ['key', 'is_active', 'owner', 'created_at', 'expiry_date']
31
+ read_only_fields = ['key', 'created_at']
32
+
33
+ class CustomUserSerializer(serializers.ModelSerializer):
34
+ class Meta:
35
+ model = CustomUser
36
+ fields = ['id', 'username', 'role']
37
+
38
+ class PushSubscriptionSerializer(serializers.Serializer):
39
+ subscription_info = serializers.JSONField()
40
+
41
+ class MarzbanAdminSerializer(serializers.ModelSerializer):
42
+ class Meta:
43
+ model = MarzbanAdmin
44
+ fields = ['id', 'username', 'password', 'permission']
45
+ extra_kwargs = {'password': {'write_only': True}}
46
+
47
+ class SecurityTokenSerializer(serializers.ModelSerializer):
48
+ class Meta:
49
+ model = SecurityToken
50
+ fields = ['admin_id', 'token', 'expiration_date']
51
+
52
+ class DiscountCodeSerializer(serializers.ModelSerializer):
53
+ class Meta:
54
+ model = DiscountCode
55
+ fields = ['id', 'code', 'discount_percentage', 'is_active']
56
+ read_only_fields = ['id', 'admin']
57
+
58
+ class PanelSerializer(serializers.ModelSerializer):
59
+ license_key = serializers.CharField(write_only=True, required=True)
60
+
61
+ class Meta:
62
+ model = Panel
63
+ fields = ['id', 'name', 'marzban_host', 'marzban_username', 'marzban_password', 'telegram_bot_token', 'license_key']
64
+ read_only_fields = ['id', 'owner']
65
+
66
+ def validate_license_key(self, value):
67
+ owner = self.context['request'].user
68
+ try:
69
+ license_obj = License.objects.get(key=value, owner=owner, is_active=True)
70
+ if hasattr(license_obj, 'linked_panel') and license_obj.linked_panel is not None:
71
+ raise serializers.ValidationError('This license key has already been used.')
72
+ return license_obj
73
+ except License.DoesNotExist:
74
+ raise serializers.ValidationError('Invalid or inactive license key.')
75
+
76
+ def create(self, validated_data):
77
+ license_obj = validated_data.pop('license_key')
78
+ panel = Panel.objects.create(
79
+ owner=self.context['request'].user,
80
+ license=license_obj,
81
+ **validated_data
82
+ )
83
+ return panel
84
+
85
+ class PlanSerializer(serializers.ModelSerializer):
86
+ class Meta:
87
+ model = Plan
88
+ fields = ['id', 'name', 'price', 'duration_days', 'data_limit_gb', 'is_active']
89
+ read_only_fields = ['id']
90
+
91
+ class SubscriptionSerializer(serializers.ModelSerializer):
92
+ end_user = serializers.CharField(source='end_user.username', read_only=True)
93
+ plan = PlanSerializer(read_only=True)
94
+
95
+ class Meta:
96
+ model = Subscription
97
+ fields = ['id', 'end_user', 'plan', 'status', 'start_date', 'end_date', 'remaining_data_gb']
98
+
99
+ class AdminLevel3SubscriptionSerializer(serializers.ModelSerializer):
100
+ end_user_username = serializers.CharField(write_only=True)
101
+ plan_id = serializers.IntegerField(write_only=True)
102
+ end_user = SubscriptionSerializer(source='end_user', read_only=True) # To show user details on response
103
+ plan = PlanSerializer(read_only=True) # To show plan details on response
104
+
105
+ class Meta:
106
+ model = Subscription
107
+ fields = ['id', 'end_user_username', 'plan_id', 'status', 'start_date', 'end_date', 'remaining_data_gb', 'end_user', 'plan']
108
+ read_only_fields = ['id', 'status', 'start_date', 'end_date', 'remaining_data_gb', 'end_user', 'plan']
109
+
110
+ def validate(self, data):
111
+ request_user = self.context['request'].user
112
+ try:
113
+ panel = request_user.owned_panel
114
+ except Panel.DoesNotExist:
115
+ raise serializers.ValidationError("Admin does not own a panel.")
116
+
117
+ try:
118
+ end_user = EndUser.objects.get(username=data.get('end_user_username'), panel=panel)
119
+ data['end_user_instance'] = end_user
120
+ except EndUser.DoesNotExist:
121
+ raise serializers.ValidationError({"end_user_username": "User not found in your panel."})
122
+
123
+ try:
124
+ plan = Plan.objects.get(id=data.get('plan_id'), is_active=True, panel=panel)
125
+ data['plan_instance'] = plan
126
+ except Plan.DoesNotExist:
127
+ raise serializers.ValidationError({"plan_id": "Plan not found or is not active in your panel."})
128
+
129
+ data['panel_instance'] = panel
130
+ return data
131
+
132
+ def create(self, validated_data):
133
+ end_user = validated_data.get('end_user_instance')
134
+ plan = validated_data.get('plan_instance')
135
+ panel = validated_data.get('panel_instance')
136
+
137
+ start_date = timezone.now().date()
138
+ end_date = start_date + datetime.timedelta(days=plan.duration_days)
139
+
140
+ subscription = Subscription.objects.create(
141
+ end_user=end_user,
142
+ plan=plan,
143
+ panel=panel,
144
+ status='active', # Assuming direct creation by admin activates it
145
+ start_date=start_date,
146
+ end_date=end_date,
147
+ remaining_data_gb=plan.data_limit_gb
148
+ )
149
+ return subscription
150
+
151
+ class PurchaseSubscriptionForUserSerializer(serializers.Serializer):
152
+ end_user_username = serializers.CharField(max_length=150)
153
+ plan_id = serializers.IntegerField()
154
+ amount = serializers.DecimalField(max_digits=10, decimal_places=2)
155
+
156
+ def validate_plan_id(self, value):
157
+ if not Plan.objects.filter(id=value, is_active=True).exists():
158
+ raise serializers.ValidationError("Plan not found or is not active.")
159
+ return value
160
+
161
+ class ReceiptUploadSerializer(serializers.ModelSerializer):
162
+ payment_token = serializers.UUIDField(write_only=True)
163
+
164
+ class Meta:
165
+ model = Payment
166
+ fields = ['receipt_image', 'receipt_text', 'payment_token']
167
+
168
+ def validate(self, data):
169
+ if not data.get('receipt_image') and not data.get('receipt_text'):
170
+ raise serializers.ValidationError("Either receipt image or text is required.")
171
+ return data
172
+
173
+ # --- Payment System Serializers ---
174
+
175
+ class PaymentMethodSerializer(serializers.ModelSerializer):
176
+ class Meta:
177
+ model = PaymentMethod
178
+ fields = ['id', 'name', 'is_active', 'can_be_managed_by_level3']
179
+ read_only_fields = ['id']
180
+
181
+ class PaymentDetailSerializer(serializers.ModelSerializer):
182
+ class Meta:
183
+ model = PaymentDetail
184
+ fields = ['id', 'card_number', 'card_holder_name', 'wallet_address']
185
+ read_only_fields = ['id']
186
+
187
+ class EndUserPaymentDetailSerializer(serializers.Serializer):
188
+ method_name = serializers.CharField(source='payment_method.get_name_display')
189
+ method_key = serializers.CharField(source='payment_method.name')
190
+ is_active = serializers.BooleanField(source='is_active')
191
+ card_number = serializers.CharField(source='admin_level_3.payment_details.card_number', read_only=True)
192
+ card_holder_name = serializers.CharField(source='admin_level_3.payment_details.card_holder_name', read_only=True)
193
+ wallet_address = serializers.CharField(source='admin_level_3.payment_details.wallet_address', read_only=True)
management/static/css/styles.css ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* management/static/css/styles.css */
2
+ @font-face {
3
+ font-family: 'Vazirmatn';
4
+ src: url('https://cdn.jsdelivr.net/gh/rastikerdar/[email protected]/fonts/web/eot/Vazirmatn-Regular.eot') format('eot'),
5
+ url('https://cdn.jsdelivr.net/gh/rastikerdar/[email protected]/fonts/web/woff2/Vazirmatn-Regular.woff2') format('woff2'),
6
+ url('https://cdn.jsdelivr.net/gh/rastikerdar/[email protected]/fonts/web/woff/Vazirmatn-Regular.woff') format('woff'),
7
+ url('https://cdn.jsdelivr.net/gh/rastikerdar/[email protected]/fonts/web/ttf/Vazirmatn-Regular.ttf') format('truetype');
8
+ font-weight: normal;
9
+ font-style: normal;
10
+ font-display: swap;
11
+ }
12
+
13
+ body {
14
+ font-family: 'Vazirmatn', sans-serif;
15
+ }
16
+
17
+ /* Custom styles for the configs list */
18
+ #configs-list li:hover {
19
+ background-color: #f1f5f9;
20
+ transition: background-color 0.2s ease-in-out;
21
+ }
22
+
management/static/js/main.js ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // management/static/js/main.js
2
+ // -*- coding: utf-8 -*-
3
+
4
+ // VAPID_PUBLIC_KEY will be passed from the Django template.
5
+ // This is a placeholder; it will be filled with the correct value in the HTML file.
6
+ const VAPID_PUBLIC_KEY_PLACEHOLDER = "{{ vapid_public_key }}";
7
+
8
+ /**
9
+ * Converts a base64 string to a Uint8Array.
10
+ * @param {string} base64String
11
+ * @returns {Uint8Array}
12
+ */
13
+ function urlBase64ToUint8Array(base64String) {
14
+ const padding = '='.repeat((4 - base64String.length % 4) % 4);
15
+ const base64 = (base64String + padding)
16
+ .replace(/\-/g, '+')
17
+ .replace(/_/g, '/');
18
+ const rawData = window.atob(base64);
19
+ const outputArray = new Uint8Array(rawData.length);
20
+ for (let i = 0; i < rawData.length; ++i) {
21
+ outputArray[i] = rawData.charCodeAt(i);
22
+ }
23
+ return outputArray;
24
+ }
25
+
26
+ /**
27
+ * Registers the service worker for the PWA.
28
+ * @returns {Promise<ServiceWorkerRegistration|null>}
29
+ */
30
+ async function registerServiceWorker() {
31
+ if ('serviceWorker' in navigator) {
32
+ try {
33
+ const registration = await navigator.serviceWorker.register('/service-worker.js');
34
+ console.log('Service worker registered successfully.');
35
+ return registration;
36
+ } catch (error) {
37
+ console.error('Service worker registration failed:', error);
38
+ return null;
39
+ }
40
+ }
41
+ console.error('Service workers are not supported in this browser.');
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Subscribes the user to push notifications.
47
+ * @param {ServiceWorkerRegistration} registration
48
+ */
49
+ async function subscribeUserToPush(registration) {
50
+ if (!('PushManager' in window)) {
51
+ console.error('Push notifications are not supported.');
52
+ return;
53
+ }
54
+
55
+ if (!VAPID_PUBLIC_KEY_PLACEHOLDER || VAPID_PUBLIC_KEY_PLACEHOLDER === "YOUR_VAPID_PUBLIC_KEY_HERE") {
56
+ console.error("VAPID public key not configured.");
57
+ return;
58
+ }
59
+
60
+ try {
61
+ const subscription = await registration.pushManager.subscribe({
62
+ userVisibleOnly: true,
63
+ applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY_PLACEHOLDER)
64
+ });
65
+
66
+ console.log('User is subscribed to push.');
67
+ const response = await fetch('/api/push/subscribe/', {
68
+ method: 'POST',
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
72
+ },
73
+ body: JSON.stringify(subscription)
74
+ });
75
+
76
+ if (!response.ok) {
77
+ console.error('Failed to save push subscription on server.');
78
+ }
79
+
80
+ } catch (error) {
81
+ console.error('Push subscription failed:', error);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Fetches and displays user dashboard data from the API.
87
+ */
88
+ async function fetchDashboardData() {
89
+ try {
90
+ const response = await fetch('/api/dashboard/');
91
+ if (!response.ok) {
92
+ throw new Error('Network response was not ok');
93
+ }
94
+ const data = await response.json();
95
+ document.getElementById('username-display').textContent = data.username;
96
+ document.getElementById('service-status').textContent = data.service_status;
97
+ document.getElementById('used-traffic').textContent = (data.used_traffic / (1024 ** 3)).toFixed(2) + ' GB';
98
+ document.getElementById('total-traffic').textContent = (data.total_traffic / (1024 ** 3)).toFixed(2) + ' GB';
99
+
100
+ const configsList = document.getElementById('configs-list');
101
+ configsList.innerHTML = ''; // Clear previous content
102
+ data.configs.forEach(config => {
103
+ const listItem = document.createElement('li');
104
+ listItem.className = 'bg-gray-50 rounded-md p-3 border border-gray-200';
105
+ listItem.innerHTML = `
106
+ <div class="font-bold text-gray-800">${config.remark}</div>
107
+ <div class="text-sm text-gray-500 break-all">${config.link}</div>
108
+ `;
109
+ configsList.appendChild(listItem);
110
+ });
111
+
112
+ } catch (error) {
113
+ console.error('Error fetching dashboard data:', error);
114
+ document.body.innerHTML = '<p class="text-red-500 text-center">خطا در بارگذاری اطلاعات داشبورد.</p>';
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Handles the generation of the Telegram security token.
120
+ */
121
+ async function handleTelegramTokenGeneration() {
122
+ const generateTokenBtn = document.getElementById('generate-token-btn');
123
+ const tokenDisplayArea = document.getElementById('token-display-area');
124
+ const telegramTokenCode = document.getElementById('telegram-token');
125
+
126
+ if (!generateTokenBtn) return;
127
+
128
+ generateTokenBtn.addEventListener('click', async () => {
129
+ const response = await fetch('/api/telegram/generate-token/', {
130
+ method: 'POST',
131
+ headers: {
132
+ 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
133
+ }
134
+ });
135
+ if (response.ok) {
136
+ const data = await response.json();
137
+ telegramTokenCode.textContent = data.token;
138
+ tokenDisplayArea.classList.remove('hidden');
139
+ } else {
140
+ // Using a custom modal or message box instead of alert()
141
+ console.error('Failed to generate token.');
142
+ alert('خطا در تولید توکن.');
143
+ }
144
+ });
145
+ }
146
+
147
+ document.addEventListener('DOMContentLoaded', async () => {
148
+ // Fetch and render dashboard data
149
+ await fetchDashboardData();
150
+
151
+ // PWA and Push Notification logic
152
+ const registration = await registerServiceWorker();
153
+ if (registration) {
154
+ if (Notification.permission === 'granted') {
155
+ console.log('Push permission already granted.');
156
+ } else if (Notification.permission === 'denied') {
157
+ console.log('Push permission denied by user.');
158
+ } else {
159
+ // Prompt the user for permission
160
+ // This is a browser feature, so we don't need a custom modal
161
+ Notification.requestPermission().then(permission => {
162
+ if (permission === 'granted') {
163
+ subscribeUserToPush(registration);
164
+ } else {
165
+ console.log('User denied push notification permission.');
166
+ }
167
+ });
168
+ }
169
+ }
170
+
171
+ // Telegram token generation
172
+ handleTelegramTokenGeneration();
173
+ });
174
+
management/static/js/service-worker.js ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // management/templates/management/service-worker.js
2
+ // -*- coding: utf-8 -*-
3
+ console.log('Service Worker Loaded and Ready.');
4
+
5
+ // CACHE_NAME را برای مدیریت نسخه‌ها تغییر دهید
6
+ const CACHE_NAME = 'marzban-pwa-cache-v1.0';
7
+
8
+ // فایل‌هایی که باید در زمان نصب Service Worker کش شوند
9
+ const urlsToCache = [
10
+ '/',
11
+ '/dashboard/',
12
+ '/static/js/main.js',
13
+ '/static/css/styles.css',
14
+ '/static/images/icons/icon-192x192.png',
15
+ '/static/images/icons/icon-512x512.png',
16
+ '/manifest.json'
17
+ ];
18
+
19
+ self.addEventListener('install', (event) => {
20
+ console.log('Service Worker: Install event.');
21
+ // در زمان نصب، فایل‌های اصلی را در حافظه کش ذخیره می‌کنیم
22
+ event.waitUntil(
23
+ caches.open(CACHE_NAME)
24
+ .then((cache) => {
25
+ console.log('Opened cache');
26
+ return cache.addAll(urlsToCache);
27
+ })
28
+ );
29
+ });
30
+
31
+ self.addEventListener('activate', (event) => {
32
+ console.log('Service Worker: Activate event.');
33
+ // پاک کردن کش‌های قدیمی
34
+ event.waitUntil(
35
+ caches.keys().then((cacheNames) => {
36
+ return Promise.all(
37
+ cacheNames.map((cacheName) => {
38
+ if (cacheName !== CACHE_NAME) {
39
+ console.log('Service Worker: Deleting old cache: ', cacheName);
40
+ return caches.delete(cacheName);
41
+ }
42
+ })
43
+ );
44
+ })
45
+ );
46
+ });
47
+
48
+ self.addEventListener('fetch', (event) => {
49
+ // برای درخواست‌های شبکه، ابتدا کش را بررسی می‌کنیم و در صورت عدم وجود، درخواست شبکه می‌زنیم
50
+ event.respondWith(
51
+ caches.match(event.request).then((response) => {
52
+ // اگر در کش بود، همان را برمی‌گردانیم
53
+ if (response) {
54
+ return response;
55
+ }
56
+ // اگر در کش نبود، درخواست شبکه می‌زنیم
57
+ return fetch(event.request);
58
+ })
59
+ );
60
+ });
61
+
62
+ self.addEventListener('push', (event) => {
63
+ const data = event.data.json();
64
+ console.log('Push received...', data);
65
+
66
+ const title = data.head || 'نوتیفیکیشن جدید';
67
+ const options = {
68
+ body: data.body,
69
+ icon: data.icon || '/static/images/icons/icon-192x192.png',
70
+ // الگوی لرزش: 200ms لرزش، 100ms مکث، 200ms لرزش
71
+ vibrate: data.vibrate || [200, 100, 200],
72
+ // پخش صدای پیش‌فرض سیستم
73
+ // توجه: پخش فایل صوتی سفارشی در مرورگرها به طور کامل پشتیبانی نمی‌شود.
74
+ // مرورگرها از صدای پیش‌فرض سیستم برای نوتیفیکیشن استفاده می‌کنند.
75
+ sound: data.sound || undefined,
76
+ badge: '/static/images/icons/badge-72x72.png'
77
+ };
78
+
79
+ event.waitUntil(
80
+ self.registration.showNotification(title, options)
81
+ );
82
+ });
83
+
84
+ self.addEventListener('notificationclick', (event) => {
85
+ console.log('Notification clicked', event);
86
+ event.notification.close();
87
+ event.waitUntil(
88
+ clients.openWindow('https://your-panel-address.com/dashboard/')
89
+ );
90
+ });
91
+
management/static/manifest.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // management/static/manifest.json
2
+ {
3
+ "name": "پنل مدیریت مرزبان",
4
+ "short_name": "مرزبان",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#ffffff",
8
+ "theme_color": "#4f46e5",
9
+ "description": "پنل مدیریت پیشرفته برای مرزبان",
10
+ "icons": [
11
+ {
12
+ "src": "/static/icons/icon-192x192.png",
13
+ "sizes": "192x192",
14
+ "type": "image/png"
15
+ },
16
+ {
17
+ "src": "/static/icons/icon-512x512.png",
18
+ "sizes": "512x512",
19
+ "type": "image/png"
20
+ }
21
+ ]
22
+ }
23
+
management/static/styles.css ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* management/static/css/styles.css */
2
+ @font-face {
3
+ font-family: 'Vazirmatn';
4
+ src: url('https://cdn.jsdelivr.net/gh/rastikerdar/[email protected]/fonts/web/eot/Vazirmatn-Regular.eot') format('eot'),
5
+ url('https://cdn.jsdelivr.net/gh/rastikerdar/[email protected]/fonts/web/woff2/Vazirmatn-Regular.woff2') format('woff2'),
6
+ url('https://cdn.jsdelivr.net/gh/rastikerdar/[email protected]/fonts/web/woff/Vazirmatn-Regular.woff') format('woff'),
7
+ url('https://cdn.jsdelivr.net/gh/rastikerdar/[email protected]/fonts/web/ttf/Vazirmatn-Regular.ttf') format('truetype');
8
+ font-weight: normal;
9
+ font-style: normal;
10
+ font-display: swap;
11
+ }
12
+
13
+ body {
14
+ font-family: 'Vazirmatn', sans-serif;
15
+ }
16
+
17
+ /* Custom styles for the configs list */
18
+ #configs-list li:hover {
19
+ background-color: #f1f5f9;
20
+ transition: background-color 0.2s ease-in-out;
21
+ }
22
+
management/tasks.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # management/tasks.py
2
+ # -*- coding: utf-8 -*-
3
+ from __future__ import absolute_import, unicode_literals
4
+ import requests
5
+ import json
6
+ import os
7
+ import datetime
8
+ from celery import shared_task
9
+ from django.db import connections, connection
10
+ from django.conf import settings
11
+ from django.utils import timezone
12
+
13
+ from .models import (
14
+ Panel,
15
+ AdminLevel2,
16
+ AdminLevel3,
17
+ EndUser,
18
+ Payment,
19
+ CustomUser,
20
+ Subscription,
21
+ )
22
+ from django.core.management import call_command
23
+
24
+ # --- Utilities ---
25
+ # FIXED: Renamed function to match imports in other files
26
+ @shared_task
27
+ def send_telegram_message(chat_id, message, bot_token):
28
+ """
29
+ Sends a message to a specific Telegram chat using a bot token.
30
+ """
31
+ if not bot_token:
32
+ print("Telegram bot token is not configured.")
33
+ return
34
+
35
+ url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
36
+ payload = {
37
+ "chat_id": chat_id,
38
+ "text": message,
39
+ "parse_mode": "Markdown"
40
+ }
41
+
42
+ try:
43
+ response = requests.post(url, json=payload)
44
+ response.raise_for_status()
45
+ print(f"Notification sent successfully to chat_id {chat_id}")
46
+ except requests.RequestException as e:
47
+ print(f"Failed to send notification to chat_id {chat_id}: {e}")
48
+
49
+ # --- Celery Tasks ---
50
+ @shared_task(name="management.tasks.schedule_reminders")
51
+ def schedule_reminders():
52
+ """
53
+ Celery task to send periodic reminders to admins and users.
54
+ """
55
+ now = timezone.now().date()
56
+
57
+ # 1. License expiry warnings for AdminLevel2s (resellers)
58
+ three_days_from_now = now + datetime.timedelta(days=3)
59
+ admin_level2s = AdminLevel2.objects.filter(license_expiry_date__lte=three_days_from_now)
60
+ for admin in admin_level2s:
61
+ # Assuming AdminLevel2 is linked to a Panel through its CustomUser owner
62
+ # This part might need adjustment based on the final model structure
63
+ try:
64
+ panel = Panel.objects.get(owner=admin.user)
65
+ if admin.telegram_chat_id and panel.telegram_bot_token:
66
+ message = (
67
+ f"🚨 **هشدار انقضای لایسنس!**\n\n"
68
+ f"ادمین {admin.user.username}، لایسنس شما در تاریخ {admin.license_expiry_date} منقضی خواهد شد. لطفاً برای تمدید آن اقدام کنید."
69
+ )
70
+ send_telegram_message(
71
+ chat_id=admin.telegram_chat_id,
72
+ message=message,
73
+ bot_token=panel.telegram_bot_token
74
+ )
75
+ except Panel.DoesNotExist:
76
+ print(f"Panel for admin {admin.user.username} not found.")
77
+
78
+
79
+ @shared_task
80
+ def send_payment_receipt_to_admin(payment_id):
81
+ """
82
+ Sends a payment receipt notification to the relevant admin via Telegram.
83
+ """
84
+ try:
85
+ payment = Payment.objects.get(id=payment_id)
86
+ admin = payment.admin
87
+ panel = payment.subscription.panel
88
+
89
+ if not admin.admin_level_3.telegram_chat_id or not panel.telegram_bot_token:
90
+ print(f"Admin telegram_chat_id or bot token not found for payment {payment_id}.")
91
+ return
92
+
93
+ message = (
94
+ f"🔔 **رسید پرداخت جدید!**\n\n"
95
+ f"یک کاربر ({payment.subscription.end_user.username}) رسید پرداختی را برای تمدید اشتراک ارسال کرده است.\n"
96
+ f"مبلغ: {payment.amount} تومان\n"
97
+ f"توکن پرداخت: `{payment.payment_token}`\n"
98
+ f"لطفا به پنل خود مراجعه کرده و پرداخت را تایید کنید."
99
+ )
100
+
101
+ if payment.receipt_image:
102
+ message += f"\n\nتصویر رسید پرداخت آپلود شده است."
103
+ elif payment.receipt_text:
104
+ message += f"\n\nمتن رسید پرداخت: {payment.receipt_text}"
105
+
106
+ send_telegram_message(
107
+ chat_id=admin.admin_level_3.telegram_chat_id,
108
+ message=message,
109
+ bot_token=panel.telegram_bot_token
110
+ )
111
+
112
+ except Payment.DoesNotExist:
113
+ print(f"Payment with id {payment_id} not found.")
114
+ except Exception as e:
115
+ print(f"Error sending payment receipt to admin: {e}")
116
+
117
+
118
+ @shared_task
119
+ def send_payment_confirmation_to_user(end_user_id, amount, status):
120
+ """
121
+ Sends a payment confirmation message to the end-user.
122
+ """
123
+ try:
124
+ end_user = EndUser.objects.get(id=end_user_id)
125
+ panel = end_user.panel
126
+
127
+ if not panel or not panel.telegram_bot_token or not end_user.telegram_chat_id:
128
+ print(f"EndUser panel, bot token or telegram_chat_id not found for user {end_user_id}.")
129
+ return
130
+
131
+ if status == 'approved':
132
+ message = (
133
+ f"✅ **پرداخت شما تأیید شد**\n\n"
134
+ f"مبلغ {amount} تومان برای پنل {panel.name} با موفقیت تأیید شد. سرویس شما به زودی فعال خواهد شد."
135
+ )
136
+ else:
137
+ message = (
138
+ f"❌ **پرداخت شما رد شد**\n\n"
139
+ f"پرداخت {amount} تومان برای پنل {panel.name} رد شد. لطفاً رسید معتبر را ارسال کنید."
140
+ )
141
+
142
+ send_telegram_message(
143
+ chat_id=end_user.telegram_chat_id,
144
+ message=message,
145
+ bot_token=panel.telegram_bot_token
146
+ )
147
+ except EndUser.DoesNotExist:
148
+ print(f"EndUser with id {end_user_id} not found.")
149
+ except Exception as e:
150
+ print(f"Error sending payment confirmation to user: {e}")
151
+
152
+ @shared_task(name="management.tasks.check_subscription_expiry")
153
+ def check_subscription_expiry():
154
+ """
155
+ Celery task to check subscription expiry and send notifications.
156
+ """
157
+ now = timezone.now().date()
158
+
159
+ # Send reminder 3 days before expiry
160
+ three_days_from_now = now + datetime.timedelta(days=3)
161
+ expiring_soon_subscriptions = Subscription.objects.filter(
162
+ end_date__date=three_days_from_now,
163
+ status='active'
164
+ )
165
+
166
+ for sub in expiring_soon_subscriptions:
167
+ if sub.end_user.telegram_chat_id and sub.panel.telegram_bot_token:
168
+ message = (
169
+ f"⏳ **اشتراک شما به زودی منقضی می‌شود!**\n\n"
170
+ f"کاربر {sub.end_user.username}، اشتراک شما در تاریخ {sub.end_date.strftime('%Y-%m-%d')} منقضی خواهد شد. برای تمدید آن از طریق بات اقدام کنید."
171
+ )
172
+ send_telegram_message(
173
+ chat_id=sub.end_user.telegram_chat_id,
174
+ message=message,
175
+ bot_token=sub.panel.telegram_bot_token
176
+ )
177
+
178
+ # Deactivate expired subscriptions
179
+ expired_subscriptions = Subscription.objects.filter(end_date__lte=now, status='active')
180
+ for sub in expired_subscriptions:
181
+ sub.status = 'expired'
182
+ sub.save()
183
+ if sub.end_user.telegram_chat_id and sub.panel.telegram_bot_token:
184
+ message = (
185
+ f"❌ **اشتراک شما منقضی شد!**\n\n"
186
+ f"کاربر {sub.end_user.username}، اشتراک شما منقضی شده است. لطفا برای تمدید اقدام کنید."
187
+ )
188
+ send_telegram_message(
189
+ chat_id=sub.end_user.telegram_chat_id,
190
+ message=message,
191
+ bot_token=sub.panel.telegram_bot_token
192
+ )
management/telebot.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # management/telebot.py
2
+ # -*- coding: utf-8 -*-
3
+ import logging
4
+ from django.contrib.auth import get_user_model
5
+ from django.conf import settings
6
+ from .models import TelegramUser
7
+ import requests
8
+ import json
9
+
10
+ logger = logging.getLogger(__name__)
11
+ CustomUser = get_user_model()
12
+
13
+ def send_telegram_message(chat_id, message):
14
+ """
15
+ ارسال پیام به یک کاربر تلگرام با استفاده از API.
16
+ """
17
+ TOKEN = settings.TELEGRAM_BOT_TOKEN
18
+ API_URL = f"https://api.telegram.org/bot{TOKEN}/sendMessage"
19
+ payload = {
20
+ 'chat_id': chat_id,
21
+ 'text': message
22
+ }
23
+ try:
24
+ response = requests.post(API_URL, json=payload)
25
+ response.raise_for_status()
26
+ logger.info(f"Message sent to chat ID {chat_id}: {message}")
27
+ except requests.exceptions.RequestException as e:
28
+ logger.error(f"Error sending message to Telegram: {e}")
29
+
30
+ def process_telegram_message(update):
31
+ """
32
+ پردازش پیام‌های ورودی از تلگرام.
33
+ """
34
+ message_data = update.get('message')
35
+ if not message_data:
36
+ return
37
+
38
+ chat_id = message_data['chat']['id']
39
+ text = message_data.get('text', '')
40
+
41
+ # بررسی اگر متن یک کد امنیتی است
42
+ # اینجا یک شرط برای طول توکن میگذاریم تا از بررسی پیام‌های کوتاه جلوگیری شود
43
+ if 10 < len(text) < 50:
44
+ # ارسال کد به سرور برای تأیید
45
+ payload = {
46
+ 'token': text,
47
+ 'chat_id': chat_id
48
+ }
49
+ try:
50
+ # URL باید به آدرس واقعی API در سرور شما اشاره کند
51
+ response = requests.post('http://127.0.0.1:8000/api/telegram/verify-token/', json=payload)
52
+ result = response.json()
53
+ send_telegram_message(chat_id, result['message'])
54
+ except requests.exceptions.RequestException as e:
55
+ logger.error(f"Error communicating with Django server: {e}")
56
+ send_telegram_message(chat_id, "متاسفم، در حال حاضر نمی‌توانم درخواست شما را پردازش کنم. لطفاً بعداً امتحان کنید.")
57
+
58
+ elif text == '/start':
59
+ send_telegram_message(chat_id, "سلام، برای همگام‌سازی حساب خود، لطفاً یک کد امنیتی از پنل کاربری خود دریافت کرده و آن را برای من ارسال کنید.")
60
+ else:
61
+ send_telegram_message(chat_id, "دستور شما نامعتبر است. لطفاً کد امنیتی خود را ارسال کنید یا از دستورات مجاز استفاده کنید.")
62
+
management/templates/management/%} ADDED
@@ -0,0 +1 @@
 
 
1
+
management/templates/management/admin-panel-management.html ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>مدیریت پنل‌های Marzban</title>
7
+ <!-- Tailwind CSS -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
11
+ body {
12
+ font-family: 'Inter', sans-serif;
13
+ }
14
+ </style>
15
+ </head>
16
+ <body class="bg-gray-100 p-8">
17
+ <div class="max-w-4xl mx-auto bg-white rounded-xl shadow-lg p-6">
18
+ <h1 class="text-3xl font-bold text-gray-800 text-center mb-6">مدیریت پنل‌های Marzban</h1>
19
+
20
+ <!-- فرم اضافه/ویرایش پنل -->
21
+ <form id="panel-form" class="space-y-4 mb-8">
22
+ <h2 id="form-title" class="text-2xl font-semibold text-gray-700">افزودن پنل جدید</h2>
23
+
24
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
25
+ <div>
26
+ <label for="panel-name" class="block text-sm font-medium text-gray-700">نام پنل</label>
27
+ <input type="text" id="panel-name" required class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
28
+ </div>
29
+ <div>
30
+ <label for="panel-address" class="block text-sm font-medium text-gray-700">آدرس پنل (URL)</label>
31
+ <input type="url" id="panel-address" required class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
32
+ </div>
33
+ </div>
34
+
35
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
36
+ <div>
37
+ <label for="db-name" class="block text-sm font-medium text-gray-700">نام دیتابیس</label>
38
+ <input type="text" id="db-name" required class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
39
+ </div>
40
+ <div>
41
+ <label for="db-user" class="block text-sm font-medium text-gray-700">نام کاربری دیتابیس</label>
42
+ <input type="text" id="db-user" required class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
43
+ </div>
44
+ </div>
45
+
46
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
47
+ <div>
48
+ <label for="db-password" class="block text-sm font-medium text-gray-700">رمز عبور دیتابیس</label>
49
+ <input type="password" id="db-password" required class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
50
+ </div>
51
+ <div>
52
+ <label for="db-host" class="block text-sm font-medium text-gray-700">آدرس هاست دیتابیس</label>
53
+ <input type="text" id="db-host" value="127.0.0.1" required class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
54
+ </div>
55
+ </div>
56
+
57
+ <div class="flex items-center space-x-2 space-x-reverse">
58
+ <input id="is-default" type="checkbox" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
59
+ <label for="is-default" class="text-sm font-medium text-gray-700">پنل پیش‌فرض باشد</label>
60
+ </div>
61
+
62
+ <div class="flex justify-start space-x-4 space-x-reverse">
63
+ <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
64
+ ذخیره
65
+ </button>
66
+ <button type="button" id="clear-form-btn" class="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
67
+ پاک کردن فرم
68
+ </button>
69
+ </div>
70
+
71
+ <div id="message-box" class="mt-4 p-3 rounded-md text-sm" style="display: none;"></div>
72
+ </form>
73
+
74
+ <hr class="my-8 border-gray-300">
75
+
76
+ <!-- جدول نمایش پنل‌ها -->
77
+ <h2 class="text-2xl font-semibold text-gray-700 mb-4">لیست پنل‌های موجود</h2>
78
+ <div class="overflow-x-auto">
79
+ <table class="min-w-full divide-y divide-gray-200">
80
+ <thead class="bg-gray-50">
81
+ <tr>
82
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">نام</th>
83
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">آدرس پنل</th>
84
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">نام دیتابیس</th>
85
+ <th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">پیش‌فرض</th>
86
+ <th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">عملیات</th>
87
+ </tr>
88
+ </thead>
89
+ <tbody id="panels-table-body" class="bg-white divide-y divide-gray-200">
90
+ <!-- محتوای پنل‌ها اینجا با جاوااسکریپت پر می‌شود -->
91
+ </tbody>
92
+ </table>
93
+ </div>
94
+ </div>
95
+
96
+ <script>
97
+ document.addEventListener('DOMContentLoaded', () => {
98
+ const API_URL = '/api/panels/';
99
+ const form = document.getElementById('panel-form');
100
+ const panelsTableBody = document.getElementById('panels-table-body');
101
+ const formTitle = document.getElementById('form-title');
102
+ const messageBox = document.getElementById('message-box');
103
+ const clearFormBtn = document.getElementById('clear-form-btn');
104
+
105
+ let editingPanelId = null;
106
+
107
+ // تابع کمکی برای گرفتن توکن JWT از localStorage
108
+ const getAuthHeaders = () => {
109
+ const accessToken = localStorage.getItem('access');
110
+ return {
111
+ 'Content-Type': 'application/json',
112
+ 'Authorization': `Bearer ${accessToken}`
113
+ };
114
+ };
115
+
116
+ // نمایش پیام‌های وضعیت (موفقیت/خطا)
117
+ const showMessage = (message, type = 'success') => {
118
+ messageBox.textContent = message;
119
+ messageBox.style.display = 'block';
120
+ if (type === 'success') {
121
+ messageBox.className = 'mt-4 p-3 rounded-md text-sm bg-green-100 text-green-800';
122
+ } else if (type === 'error') {
123
+ messageBox.className = 'mt-4 p-3 rounded-md text-sm bg-red-100 text-red-800';
124
+ }
125
+ setTimeout(() => {
126
+ messageBox.style.display = 'none';
127
+ }, 5000);
128
+ };
129
+
130
+ // پاک کردن فرم
131
+ const clearForm = () => {
132
+ form.reset();
133
+ formTitle.textContent = 'افزودن پنل جدید';
134
+ editingPanelId = null;
135
+ };
136
+ clearFormBtn.addEventListener('click', clearForm);
137
+
138
+ // واکشی لیست پنل‌ها از API
139
+ const fetchPanels = async () => {
140
+ try {
141
+ const response = await fetch(API_URL, { headers: getAuthHeaders() });
142
+ if (!response.ok) {
143
+ throw new Error('Failed to fetch panels');
144
+ }
145
+ const panels = await response.json();
146
+ renderPanels(panels);
147
+ } catch (error) {
148
+ showMessage(`خطا در دریافت لیست پنل‌ها: ${error.message}`, 'error');
149
+ }
150
+ };
151
+
152
+ // نمایش پنل‌ها در جدول
153
+ const renderPanels = (panels) => {
154
+ panelsTableBody.innerHTML = '';
155
+ panels.forEach(panel => {
156
+ const row = document.createElement('tr');
157
+ row.className = 'hover:bg-gray-100';
158
+ row.innerHTML = `
159
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${panel.name}</td>
160
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${panel.panel_address}</td>
161
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${panel.db_name}</td>
162
+ <td class="px-6 py-4 whitespace-nowrap text-center text-sm text-gray-500">${panel.is_default ? '✅' : '❌'}</td>
163
+ <td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
164
+ <button data-id="${panel.id}" class="edit-btn text-indigo-600 hover:text-indigo-900 mx-2">ویرایش</button>
165
+ <button data-id="${panel.id}" class="delete-btn text-red-600 hover:text-red-900 mx-2">حذف</button>
166
+ </td>
167
+ `;
168
+ panelsTableBody.appendChild(row);
169
+ });
170
+
171
+ // اضافه کردن event listener برای دکمه‌های ویرایش و حذف
172
+ document.querySelectorAll('.edit-btn').forEach(button => {
173
+ button.addEventListener('click', (e) => handleEditClick(e.target.dataset.id));
174
+ });
175
+ document.querySelectorAll('.delete-btn').forEach(button => {
176
+ button.addEventListener('click', (e) => handleDeleteClick(e.target.dataset.id));
177
+ });
178
+ };
179
+
180
+ // پر کردن فرم برای ویرایش یک پنل
181
+ const handleEditClick = async (panelId) => {
182
+ try {
183
+ const response = await fetch(`${API_URL}${panelId}/`, { headers: getAuthHeaders() });
184
+ if (!response.ok) {
185
+ throw new Error('Failed to fetch panel details');
186
+ }
187
+ const panel = await response.json();
188
+
189
+ formTitle.textContent = `ویرایش پنل: ${panel.name}`;
190
+ editingPanelId = panel.id;
191
+
192
+ document.getElementById('panel-name').value = panel.name;
193
+ document.getElementById('panel-address').value = panel.panel_address;
194
+ document.getElementById('db-name').value = panel.db_name;
195
+ document.getElementById('db-user').value = panel.db_user;
196
+ document.getElementById('db-password').value = panel.db_password;
197
+ document.getElementById('db-host').value = panel.db_host;
198
+ document.getElementById('is-default').checked = panel.is_default;
199
+ } catch (error) {
200
+ showMessage(`خطا در دریافت اطلاعات پنل برای ویرایش: ${error.message}`, 'error');
201
+ }
202
+ };
203
+
204
+ // ارسال درخواست حذف
205
+ const handleDeleteClick = async (panelId) => {
206
+ if (window.confirm("آیا از حذف این پنل اطمینان دارید؟")) {
207
+ try {
208
+ const response = await fetch(`${API_URL}${panelId}/`, {
209
+ method: 'DELETE',
210
+ headers: getAuthHeaders()
211
+ });
212
+ if (response.status === 204) {
213
+ showMessage('پنل با موفقیت حذف شد.');
214
+ fetchPanels();
215
+ } else {
216
+ const errorData = await response.json();
217
+ showMessage(`خطا در حذف پنل: ${errorData.error}`, 'error');
218
+ }
219
+ } catch (error) {
220
+ showMessage(`خطا در حذف پنل: ${error.message}`, 'error');
221
+ }
222
+ }
223
+ };
224
+
225
+ // هندل کردن ارسال فرم (ایجاد یا ویرایش)
226
+ form.addEventListener('submit', async (e) => {
227
+ e.preventDefault();
228
+
229
+ const formData = {
230
+ name: document.getElementById('panel-name').value,
231
+ panel_address: document.getElementById('panel-address').value,
232
+ db_name: document.getElementById('db-name').value,
233
+ db_user: document.getElementById('db-user').value,
234
+ db_password: document.getElementById('db-password').value,
235
+ db_host: document.getElementById('db-host').value,
236
+ db_port: 3306, // Default port
237
+ is_default: document.getElementById('is-default').checked,
238
+ };
239
+
240
+ let response;
241
+ try {
242
+ if (editingPanelId) {
243
+ response = await fetch(`${API_URL}${editingPanelId}/`, {
244
+ method: 'PUT',
245
+ headers: getAuthHeaders(),
246
+ body: JSON.stringify(formData)
247
+ });
248
+ } else {
249
+ response = await fetch(API_URL, {
250
+ method: 'POST',
251
+ headers: getAuthHeaders(),
252
+ body: JSON.stringify(formData)
253
+ });
254
+ }
255
+
256
+ if (response.ok) {
257
+ const result = await response.json();
258
+ showMessage(editingPanelId ? 'پنل با موفقیت به‌روزرسانی شد.' : 'پنل جدید با موفقیت ایجاد شد.');
259
+ clearForm();
260
+ fetchPanels();
261
+ } else {
262
+ const errorData = await response.json();
263
+ const errorMsg = errorData.error || JSON.stringify(errorData);
264
+ showMessage(`خطا در عملیات: ${errorMsg}`, 'error');
265
+ }
266
+ } catch (error) {
267
+ showMessage(`خطا در ارتباط با سرور: ${error.message}`, 'error');
268
+ }
269
+ });
270
+
271
+ fetchPanels();
272
+ });
273
+ </script>
274
+ </body>
275
+ </html>
276
+
management/templates/management/admin_dashboard.html ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'management/base.html' %}
2
+ {% load static %}
3
+
4
+ {% block header_title %}داشبورد ادمین {{ request.user.username }}{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="row">
8
+ <div class="col-md-4">
9
+ <div class="card text-center">
10
+ <div class="card-body">
11
+ <h5 class="card-title">تعداد کاربران</h5>
12
+ <h2 class="card-text">{{ total_users }}</h2>
13
+ </div>
14
+ </div>
15
+ </div>
16
+ <div class="col-md-4">
17
+ <div class="card text-center">
18
+ <div class="card-body">
19
+ <h5 class="card-title">پرداخت‌های در انتظار تأیید</h5>
20
+ <h2 class="card-text">{{ pending_payments }}</h2>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ <div class="col-md-4">
25
+ <div class="card text-center">
26
+ <div class="card-body">
27
+ <h5 class="card-title">کاربران بدون پرداخت (۴۸ ساعت)</h5>
28
+ <h2 class="card-text">{{ users_no_payment_48h }}</h2>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ {% endblock %}
34
+
35
+
management/templates/management/admin_login.html ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ورود به پنل ادمین</title>
7
+ <style>
8
+ body {
9
+ display: flex;
10
+ flex-direction: column;
11
+ justify-content: center;
12
+ align-items: center;
13
+ height: 100vh;
14
+ background-color: #f0f2f5;
15
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
16
+ text-align: center;
17
+ }
18
+ .login-container {
19
+ background: white;
20
+ padding: 30px;
21
+ border-radius: 8px;
22
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
23
+ width: 300px;
24
+ }
25
+ h2 {
26
+ margin-bottom: 20px;
27
+ color: #333;
28
+ }
29
+ .form-group {
30
+ margin-bottom: 15px;
31
+ text-align: right;
32
+ }
33
+ .form-group label {
34
+ display: block;
35
+ margin-bottom: 5px;
36
+ font-size: 14px;
37
+ color: #555;
38
+ }
39
+ .form-group input {
40
+ width: 100%;
41
+ padding: 10px;
42
+ box-sizing: border-box;
43
+ border: 1px solid #ccc;
44
+ border-radius: 5px;
45
+ font-size: 16px;
46
+ }
47
+ .error-message {
48
+ color: #dc3545;
49
+ margin-bottom: 15px;
50
+ }
51
+ .submit-button {
52
+ width: 100%;
53
+ padding: 12px 20px;
54
+ margin-top: 20px;
55
+ border-radius: 5px;
56
+ border: none;
57
+ background-color: #007bff; /* Changed color for admin */
58
+ color: white;
59
+ font-size: 16px;
60
+ cursor: pointer;
61
+ transition: background-color 0.3s ease;
62
+ }
63
+ .submit-button:hover {
64
+ background-color: #0056b3;
65
+ }
66
+ .messages {
67
+ list-style-type: none;
68
+ padding: 0;
69
+ margin-bottom: 20px;
70
+ }
71
+ .messages .error {
72
+ background-color: #f8d7da;
73
+ color: #721c24;
74
+ border: 1px solid #f5c6cb;
75
+ padding: 10px;
76
+ border-radius: 5px;
77
+ }
78
+ .messages .success {
79
+ background-color: #d4edda;
80
+ color: #155724;
81
+ border: 1px solid #c3e6cb;
82
+ padding: 10px;
83
+ border-radius: 5px;
84
+ }
85
+ </style>
86
+ </head>
87
+ <body>
88
+ <div class="login-container">
89
+ <h2>ورود به پنل ادمین</h2>
90
+ {% if messages %}
91
+ <ul class="messages">
92
+ {% for message in messages %}
93
+ <li class="{{ message.tags }}">{{ message }}</li>
94
+ {% endfor %}
95
+ </ul>
96
+ {% endif %}
97
+ <form method="post" action="{% url 'admin_login_view' %}">
98
+ {% csrf_token %}
99
+ <div class="form-group">
100
+ <label for="username">نام کاربری:</label>
101
+ <input type="text" id="username" name="username" required>
102
+ </div>
103
+ <div class="form-group">
104
+ <label for="password">رمز عبور:</label>
105
+ <input type="password" id="password" name="password" required>
106
+ </div>
107
+ <button type="submit" class="submit-button">ورود</button>
108
+ </form>
109
+ </div>
110
+ </body>
111
+ </html>
112
+
management/templates/management/amin_login.html ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ورود ادمین</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
11
+ background-color: #f0f2f5;
12
+ }
13
+ </style>
14
+ </head>
15
+ <body class="flex items-center justify-center min-h-screen">
16
+ <div class="bg-white p-8 rounded-lg shadow-md w-full max-w-sm">
17
+ <h2 class="text-2xl font-bold text-center mb-6 text-gray-800">ورود به پنل ادمین</h2>
18
+
19
+ {% if messages %}
20
+ <ul class="mb-4">
21
+ {% for message in messages %}
22
+ <li class="p-2 {% if message.tags %} bg-{{ message.tags }}-100 text-{{ message.tags }}-800 {% endif %} rounded">{{ message }}</li>
23
+ {% endfor %}
24
+ </ul>
25
+ {% endif %}
26
+
27
+ <form method="post" action="{% url 'management:admin_login' %}" class="space-y-4">
28
+ {% csrf_token %}
29
+ <div>
30
+ <label for="{{ form.username.id_for_label }}" class="block text-sm font-medium text-gray-700">نام کاربری:</label>
31
+ {{ form.username }}
32
+ </div>
33
+ <div>
34
+ <label for="{{ form.password.id_for_label }}" class="block text-sm font-medium text-gray-700">رمز عبور:</label>
35
+ {{ form.password }}
36
+ </div>
37
+ <button type="submit" class="w-full py-2 px-4 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 transition duration-300">
38
+ ورود
39
+ </button>
40
+ </form>
41
+ </div>
42
+ </body>
43
+ </html>
44
+
management/templates/management/base.html ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- management/templates/management/base.html -->
2
+ <!-- -*- coding: utf-8 -*- -->
3
+ <!DOCTYPE html>
4
+ <html lang="fa" dir="rtl">
5
+ <head>
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>{% block title %}پنل مدیریت{% endblock %}</title>
9
+ <!-- Add link to the PWA manifest file -->
10
+ <link rel="manifest" href="/static/manifest.json">
11
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css">
12
+ <style>
13
+ body {
14
+ font-family: 'Vazirmatn', sans-serif;
15
+ background-color: #f3f4f6;
16
+ }
17
+ </style>
18
+ </head>
19
+ <body class="bg-gray-100 p-8">
20
+ <div class="max-w-4xl mx-auto bg-white shadow-xl rounded-lg p-6">
21
+ {% block content %}{% endblock %}
22
+ </div>
23
+ </body>
24
+ </html>
25
+
management/templates/management/create_discount_code.html ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- management/templates/management/create_discount_code.html -->
2
+ <!-- -*- coding: utf-8 -*- -->
3
+ <!DOCTYPE html>
4
+ <html lang="fa" dir="rtl">
5
+ <head>
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>ایجاد کد تخفیف</title>
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css">
10
+ <style>
11
+ body {
12
+ font-family: 'Vazirmatn', sans-serif;
13
+ background-color: #f3f4f6;
14
+ }
15
+ </style>
16
+ </head>
17
+ <body class="bg-gray-100 p-8">
18
+ <div class="max-w-xl mx-auto bg-white shadow-xl rounded-lg p-6">
19
+ <header class="border-b pb-4 mb-4">
20
+ <h1 class="text-3xl font-bold text-gray-800">ایجاد کد تخفیف</h1>
21
+ </header>
22
+
23
+ <main>
24
+ {% if messages %}
25
+ <ul class="mb-4">
26
+ {% for message in messages %}
27
+ <li class="p-3 rounded-lg {% if message.tags == 'error' %}bg-red-100 text-red-800{% else %}bg-green-100 text-green-800{% endif %}">
28
+ {{ message }}
29
+ </li>
30
+ {% endfor %}
31
+ </ul>
32
+ {% endif %}
33
+
34
+ <form method="post">
35
+ {% csrf_token %}
36
+ <div class="space-y-4">
37
+ {% for field in form %}
38
+ <div>
39
+ <label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-700">{{ field.label }}</label>
40
+ <div class="mt-1">
41
+ {{ field }}
42
+ </div>
43
+ {% if field.errors %}
44
+ {% for error in field.errors %}
45
+ <p class="mt-2 text-sm text-red-600">{{ error }}</p>
46
+ {% endfor %}
47
+ {% endif %}
48
+ </div>
49
+ {% endfor %}
50
+ </div>
51
+
52
+ <div class="mt-6">
53
+ <button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg">
54
+ ایجاد کد تخفیف
55
+ </button>
56
+ </div>
57
+ </form>
58
+ </main>
59
+ </div>
60
+ </body>
61
+ </html>
62
+
management/templates/management/create_notification.html ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'management/base.html' %}
2
+ {% load static %}
3
+
4
+ {% block header_title %}ارسال اعلان جدید{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="card shadow-sm">
8
+ <div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
9
+ <h5 class="mb-0">ارسال اعلان جدید</h5>
10
+ <a href="{% url 'notification_history_view' %}" class="btn btn-light btn-sm">تاریخچه اعلان‌ها</a>
11
+ </div>
12
+ <div class="card-body">
13
+ <form action="{% url 'send_notification_view' %}" method="post">
14
+ {% csrf_token %}
15
+
16
+ <!-- انتخاب نوع اعلان (دستی یا خودکار) -->
17
+ <div class="mb-3">
18
+ <label class="form-label">نوع اعلان</label>
19
+ <div class="form-check form-check-inline">
20
+ <input class="form-check-input" type="radio" name="notification_type" id="type_manual" value="manual" checked>
21
+ <label class="form-check-label" for="type_manual">ارسال دستی</label>
22
+ </div>
23
+ <div class="form-check form-check-inline">
24
+ <input class="form-check-input" type="radio" name="notification_type" id="type_automatic" value="automatic">
25
+ <label class="form-check-label" for="type_automatic">ارسال خودکار</label>
26
+ </div>
27
+ </div>
28
+
29
+ <!-- بخش مربوط به اعلان دستی -->
30
+ <div id="manual-notification-fields">
31
+ <div class="mb-3">
32
+ <label for="id_title" class="form-label">عنوان</label>
33
+ <input type="text" class="form-control" id="id_title" name="title" required>
34
+ </div>
35
+ <div class="mb-3">
36
+ <label for="id_content" class="form-label">متن اعلان</label>
37
+ <textarea class="form-control" id="id_content" name="content" rows="5" required></textarea>
38
+ </div>
39
+
40
+ <!-- گزینه‌های اعلان غنی -->
41
+ <div class="card bg-light mb-3">
42
+ <div class="card-body">
43
+ <h6 class="card-title">گزینه‌های پیشرفته (اختیاری)</h6>
44
+ <div class="row">
45
+ <div class="col-md-6 mb-3">
46
+ <label for="id_button_text" class="form-label">متن دکمه</label>
47
+ <input type="text" class="form-control" id="id_button_text" name="button_text">
48
+ </div>
49
+ <div class="col-md-6 mb-3">
50
+ <label for="id_button_url" class="form-label">لینک دکمه</label>
51
+ <input type="url" class="form-control" id="id_button_url" name="button_url">
52
+ </div>
53
+ </div>
54
+ <div class="mb-3">
55
+ <label for="id_image_url" class="form-label">لینک تصویر/آیکون</label>
56
+ <input type="url" class="form-control" id="id_image_url" name="image_url">
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ <!-- گیرنده اعلان دستی -->
62
+ <div class="mb-3">
63
+ <label class="form-label">گیرنده اعلان</label>
64
+ <div class="form-check">
65
+ <input class="form-check-input" type="radio" name="recipient_type" id="recipient_all" value="all" checked>
66
+ <label class="form-check-label" for="recipient_all">همه کاربران</label>
67
+ </div>
68
+ <div class="form-check">
69
+ <input class="form-check-input" type="radio" name="recipient_type" id="recipient_specific" value="specific">
70
+ <label class="form-check-label" for="recipient_specific">کاربران خاص</label>
71
+ </div>
72
+ </div>
73
+ <div class="mb-3" id="specific-users-container">
74
+ <label for="id_users" class="form-label">انتخاب کاربران (می‌توانید چند کاربر را انتخاب کنید)</label>
75
+ <select multiple class="form-select" id="id_users" name="users">
76
+ {% for user in all_users %}
77
+ <option value="{{ user.id }}">{{ user.full_name }} ({{ user.username }})</option>
78
+ {% endfor %}
79
+ </select>
80
+ </div>
81
+
82
+ <!-- زمان‌بندی اعلان دستی -->
83
+ <div class="mb-3">
84
+ <label for="id_schedule" class="form-label">زمان‌بندی ارسال</label>
85
+ <div class="input-group">
86
+ <select class="form-select" id="schedule-type-select" name="schedule_type">
87
+ <option value="one_time">یک‌بار</option>
88
+ <option value="weekly">هفتگی</option>
89
+ <option value="monthly">ماهانه</option>
90
+ </select>
91
+ <input type="datetime-local" class="form-control" id="id_schedule_time" name="schedule_time" required>
92
+ </div>
93
+ </div>
94
+
95
+ <div class="mb-3 form-check">
96
+ <input type="checkbox" class="form-check-input" id="id_is_urgent" name="is_urgent">
97
+ <label class="form-check-label" for="id_is_urgent">اخطار فوری</label>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- بخش مربوط به اعلان خودکار -->
102
+ <div id="automatic-notification-fields" style="display: none;">
103
+ <div class="card bg-light mb-3">
104
+ <div class="card-body">
105
+ <h6 class="card-title">قانون ارسال اعلان خودکار</h6>
106
+ <div class="mb-3 form-check">
107
+ <input type="checkbox" class="form-check-input" id="id_auto_consumption" name="auto_consumption">
108
+ <label class="form-check-label" for="id_auto_consumption">ارسال به هنگام رسیدن مصرف به یک حد مشخص</label>
109
+ </div>
110
+ <div class="input-group mb-3" id="consumption-threshold-container" style="display: none;">
111
+ <input type="number" class="form-control" name="consumption_threshold" placeholder="مثلاً ۹۰" min="1" max="100">
112
+ <span class="input-group-text">%</span>
113
+ </div>
114
+ <div class="mb-3 form-check">
115
+ <input type="checkbox" class="form-check-input" id="id_auto_expiration" name="auto_expiration">
116
+ <label class="form-check-label" for="id_auto_expiration">ارسال قبل از تاریخ انقضا</label>
117
+ </div>
118
+ <div class="input-group mb-3" id="expiration-days-container" style="display: none;">
119
+ <input type="number" class="form-control" name="expiration_days_before" placeholder="مثلاً ۳" min="1">
120
+ <span class="input-group-text">روز قبل</span>
121
+ </div>
122
+ <div class="mb-3">
123
+ <label for="id_auto_plans" class="form-label">مربوط به پلن‌های زیر</label>
124
+ <select multiple class="form-select" id="id_auto_plans" name="auto_plans">
125
+ {% for plan in all_plans %}
126
+ <option value="{{ plan.id }}">{{ plan.name }}</option>
127
+ {% endfor %}
128
+ </select>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ <button type="submit" class="btn btn-primary mt-3">ارسال اعلان</button>
135
+ </form>
136
+ </div>
137
+ </div>
138
+ <script>
139
+ document.addEventListener('DOMContentLoaded', () => {
140
+ const typeManual = document.getElementById('type_manual');
141
+ const typeAutomatic = document.getElementById('type_automatic');
142
+ const manualFields = document.getElementById('manual-notification-fields');
143
+ const automaticFields = document.getElementById('automatic-notification-fields');
144
+
145
+ const recipientTypeRadios = document.querySelectorAll('input[name="recipient_type"]');
146
+ const specificUsersContainer = document.getElementById('specific-users-container');
147
+
148
+ const autoConsumptionCheckbox = document.getElementById('id_auto_consumption');
149
+ const consumptionThresholdContainer = document.getElementById('consumption-threshold-container');
150
+
151
+ const autoExpirationCheckbox = document.getElementById('id_auto_expiration');
152
+ const expirationDaysContainer = document.getElementById('expiration-days-container');
153
+
154
+ // نمایش/پنهان کردن فیلدهای اعلان دستی و خودکار
155
+ function toggleNotificationType() {
156
+ if (typeManual.checked) {
157
+ manualFields.style.display = 'block';
158
+ automaticFields.style.display = 'none';
159
+ } else {
160
+ manualFields.style.display = 'none';
161
+ automaticFields.style.display = 'block';
162
+ }
163
+ }
164
+ typeManual.addEventListener('change', toggleNotificationType);
165
+ typeAutomatic.addEventListener('change', toggleNotificationType);
166
+
167
+ // نمایش/پنهان کردن فیلد انتخاب کاربران خاص
168
+ recipientTypeRadios.forEach(radio => {
169
+ radio.addEventListener('change', (event) => {
170
+ if (event.target.value === 'specific') {
171
+ specificUsersContainer.style.display = 'block';
172
+ } else {
173
+ specificUsersContainer.style.display = 'none';
174
+ }
175
+ });
176
+ });
177
+
178
+ // نمایش/پنهان کردن فیلد میزان مصرف
179
+ autoConsumptionCheckbox.addEventListener('change', (event) => {
180
+ if (event.target.checked) {
181
+ consumptionThresholdContainer.style.display = 'flex';
182
+ } else {
183
+ consumptionThresholdContainer.style.display = 'none';
184
+ }
185
+ });
186
+
187
+ // نمایش/پنهان کردن فیلد روزهای قبل از انقضا
188
+ autoExpirationCheckbox.addEventListener('change', (event) => {
189
+ if (event.target.checked) {
190
+ expirationDaysContainer.style.display = 'flex';
191
+ } else {
192
+ expirationDaysContainer.style.display = 'none';
193
+ }
194
+ });
195
+ });
196
+ </script>
197
+ {% endblock %}
198
+
management/templates/management/error.html ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>خطا</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ background-color: #f8d7da;
11
+ color: #721c24;
12
+ display: flex;
13
+ justify-content: center;
14
+ align-items: center;
15
+ height: 100vh;
16
+ margin: 0;
17
+ text-align: center;
18
+ }
19
+ .container {
20
+ background-color: #fdd;
21
+ padding: 40px;
22
+ border-radius: 10px;
23
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
24
+ max-width: 500px;
25
+ border: 1px solid #f5c6cb;
26
+ }
27
+ h1 {
28
+ color: #dc3545;
29
+ }
30
+ p {
31
+ line-height: 1.6;
32
+ margin-top: 20px;
33
+ }
34
+ </style>
35
+ </head>
36
+ <body>
37
+ <div class="container">
38
+ <h1>خطا!</h1>
39
+ <p>{{ message }}</p>
40
+ <a href="/">بازگشت به صفحه اصلی</a>
41
+ </div>
42
+ </body>
43
+ </html>
management/templates/management/home.html ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>صفحه اصلی</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
11
+ background-color: #f0f2f5;
12
+ }
13
+ </style>
14
+ </head>
15
+ <body class="flex flex-col items-center justify-center min-h-screen">
16
+ <div class="bg-white p-8 rounded-lg shadow-md w-full max-w-sm text-center">
17
+ <h1 class="text-3xl font-bold mb-6 text-gray-800">به پنل خوش آمدید</h1>
18
+ <p class="text-gray-600 mb-8">لطفاً نوع کاربری خود را انتخاب کنید.</p>
19
+ <div class="space-y-4">
20
+ <a href="{% url 'management:admin_login' %}" class="block w-full py-3 px-6 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 transition duration-300">
21
+ ورود به پنل ادمین
22
+ </a>
23
+ <a href="{% url 'management:user_dashboard_login' %}" class="block w-full py-3 px-6 bg-gray-600 text-white font-semibold rounded-lg shadow-md hover:bg-gray-700 transition duration-300">
24
+ ورود به پنل کاربر
25
+ </a>
26
+ </div>
27
+ </div>
28
+ </body>
29
+ </html>
30
+