Spaces:
Running
Running
Upload 73 files
#340
by
coerxso
- opened
This view is limited to 50 files because it contains too many changes.
See the raw diff here.
- key.py +32 -0
- manage.py +20 -0
- management/__init__.py +0 -0
- management/__pycache__/__init__.cpython-312.pyc +0 -0
- management/__pycache__/admin.cpython-312.pyc +0 -0
- management/__pycache__/apps.cpython-312.pyc +0 -0
- management/__pycache__/backends.cpython-312.pyc +0 -0
- management/__pycache__/forms.cpython-312.pyc +0 -0
- management/__pycache__/marzban_api.cpython-312.pyc +0 -0
- management/__pycache__/marzban_client.cpython-312.pyc +0 -0
- management/__pycache__/models.cpython-312.pyc +0 -0
- management/__pycache__/serializers.cpython-312.pyc +0 -0
- management/__pycache__/tasks.cpython-312.pyc +0 -0
- management/__pycache__/urls.cpython-312.pyc +0 -0
- management/__pycache__/utils.cpython-312.pyc +0 -0
- management/__pycache__/views.cpython-312.pyc +0 -0
- management/admin.py +130 -0
- management/backends.py +89 -0
- management/forms.py +13 -0
- management/manage.py +20 -0
- management/management/commands/backup_all_databases.py +100 -0
- management/management/commands/check_payments.py +42 -0
- management/management/commands/restore_all_databases.py +100 -0
- management/management/commands/schedule_reminders.py +203 -0
- management/marzban_client.py +79 -0
- management/migrations/0001_initial.py +241 -0
- management/migrations/__init__.py +0 -0
- management/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- management/migrations/__pycache__/0002_remove_user_admin_remove_payment_user_and_more.cpython-312.pyc +0 -0
- management/migrations/__pycache__/0003_notification_remove_payment_user_config_and_more.cpython-312.pyc +0 -0
- management/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- management/models.py +261 -0
- management/serializers.py +193 -0
- management/static/css/styles.css +22 -0
- management/static/js/main.js +174 -0
- management/static/js/service-worker.js +91 -0
- management/static/manifest.json +23 -0
- management/static/styles.css +22 -0
- management/tasks.py +192 -0
- management/telebot.py +62 -0
- management/templates/management/%} +1 -0
- management/templates/management/admin-panel-management.html +276 -0
- management/templates/management/admin_dashboard.html +35 -0
- management/templates/management/admin_login.html +112 -0
- management/templates/management/amin_login.html +44 -0
- management/templates/management/base.html +25 -0
- management/templates/management/create_discount_code.html +62 -0
- management/templates/management/create_notification.html +198 -0
- management/templates/management/error.html +43 -0
- 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 |
+
|