mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-08-21 14:37:16 +00:00
Fixed multiuser outline and xray .
This commit is contained in:
172
vpn/admin.py
172
vpn/admin.py
@@ -19,6 +19,15 @@ from vpn.models import User, ACL, ACLLink
|
||||
from vpn.forms import UserForm
|
||||
from mysite.settings import EXTERNAL_ADDRESS
|
||||
from django.db.models import Max, Subquery, OuterRef, Q
|
||||
|
||||
|
||||
def format_bytes(bytes_val):
|
||||
"""Format bytes to human readable format"""
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if bytes_val < 1024.0:
|
||||
return f"{bytes_val:.1f}{unit}"
|
||||
bytes_val /= 1024.0
|
||||
return f"{bytes_val:.1f}PB"
|
||||
from .server_plugins import (
|
||||
Server,
|
||||
WireguardServer,
|
||||
@@ -802,7 +811,25 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
||||
|
||||
user_count = obj.user_count if hasattr(obj, 'user_count') else 0
|
||||
|
||||
# Use prefetched data if available
|
||||
# Different logic for Xray vs legacy servers
|
||||
if obj.server_type == 'xray_v2':
|
||||
# For Xray servers, count inbounds and active subscriptions
|
||||
from vpn.models_xray import ServerInbound
|
||||
total_inbounds = ServerInbound.objects.filter(server=obj, active=True).count()
|
||||
|
||||
# Count recent subscription accesses via AccessLog
|
||||
thirty_days_ago = timezone.now() - timedelta(days=30)
|
||||
from vpn.models import AccessLog
|
||||
active_accesses = AccessLog.objects.filter(
|
||||
server='Xray-Subscription',
|
||||
action='Success',
|
||||
timestamp__gte=thirty_days_ago
|
||||
).values('user').distinct().count()
|
||||
|
||||
total_links = total_inbounds
|
||||
active_links = min(active_accesses, user_count) # Can't be more than total users
|
||||
else:
|
||||
# Legacy servers: use ACL links as before
|
||||
if hasattr(obj, 'acl_set'):
|
||||
all_links = []
|
||||
for acl in obj.acl_set.all():
|
||||
@@ -829,14 +856,65 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
||||
if user_count == 0:
|
||||
color = '#9ca3af' # gray - no users
|
||||
elif total_links == 0:
|
||||
color = '#dc2626' # red - no links
|
||||
elif total_links > 0 and active_links > total_links * 0.7: # High activity
|
||||
color = '#dc2626' # red - no links/inbounds
|
||||
elif obj.server_type == 'xray_v2':
|
||||
# For Xray: base on user activity rather than link activity
|
||||
if active_links > user_count * 0.5: # More than half users active
|
||||
color = '#16a34a' # green
|
||||
elif active_links > user_count * 0.2: # More than 20% users active
|
||||
color = '#eab308' # yellow
|
||||
else:
|
||||
color = '#f97316' # orange - low activity
|
||||
else:
|
||||
# Legacy servers: base on link activity
|
||||
if total_links > 0 and active_links > total_links * 0.7: # High activity
|
||||
color = '#16a34a' # green
|
||||
elif total_links > 0 and active_links > total_links * 0.3: # Medium activity
|
||||
color = '#eab308' # yellow
|
||||
else:
|
||||
color = '#f97316' # orange - low activity
|
||||
|
||||
# Different display for Xray vs legacy
|
||||
if obj.server_type == 'xray_v2':
|
||||
# Try to get traffic stats if stats enabled
|
||||
traffic_info = ""
|
||||
# Get the real XrayServerV2 instance to access its fields
|
||||
xray_server = obj.get_real_instance()
|
||||
if hasattr(xray_server, 'stats_enabled') and xray_server.stats_enabled and xray_server.api_enabled:
|
||||
try:
|
||||
from vpn.xray_api_v2.client import XrayClient
|
||||
from vpn.xray_api_v2.stats import StatsManager
|
||||
|
||||
client = XrayClient(server=xray_server.api_address)
|
||||
stats_manager = StatsManager(client)
|
||||
traffic_summary = stats_manager.get_traffic_summary()
|
||||
|
||||
# Calculate total traffic
|
||||
total_uplink = 0
|
||||
total_downlink = 0
|
||||
|
||||
# Sum up user traffic
|
||||
for user_email, user_traffic in traffic_summary.get('users', {}).items():
|
||||
total_uplink += user_traffic.get('uplink', 0)
|
||||
total_downlink += user_traffic.get('downlink', 0)
|
||||
|
||||
# Format traffic
|
||||
|
||||
if total_uplink > 0 or total_downlink > 0:
|
||||
traffic_info = f'<div style="color: #6b7280; font-size: 11px;">↑{format_bytes(total_uplink)} ↓{format_bytes(total_downlink)}</div>'
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.debug(f"Could not get stats for Xray server {xray_server.name}: {e}")
|
||||
|
||||
return mark_safe(
|
||||
f'<div style="font-size: 12px;">' +
|
||||
f'<div style="color: {color}; font-weight: bold;">👥 {user_count} users</div>' +
|
||||
f'<div style="color: #6b7280;">📡 {total_links} inbounds</div>' +
|
||||
traffic_info +
|
||||
f'</div>'
|
||||
)
|
||||
else:
|
||||
return mark_safe(
|
||||
f'<div style="font-size: 12px;">' +
|
||||
f'<div style="color: {color}; font-weight: bold;">👥 {user_count} users</div>' +
|
||||
@@ -844,6 +922,10 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
||||
f'</div>'
|
||||
)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Error in user_stats for server {obj.name}: {e}", exc_info=True)
|
||||
return mark_safe(f'<span style="color: #dc2626; font-size: 11px;">Stats error: {e}</span>')
|
||||
|
||||
@admin.display(description='Activity')
|
||||
@@ -896,8 +978,46 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
from django.db.models import Case, When, Value, IntegerField, F, Q, Subquery, OuterRef
|
||||
from vpn.models_xray import UserSubscription, ServerInbound
|
||||
|
||||
qs = super().get_queryset(request)
|
||||
qs = qs.annotate(user_count=Count('acl'))
|
||||
|
||||
# Count ACL users for all servers
|
||||
qs = qs.annotate(
|
||||
acl_user_count=Count('acl__user', distinct=True)
|
||||
)
|
||||
|
||||
# For Xray servers, calculate user count separately
|
||||
# Create subquery to count Xray users
|
||||
xray_user_count_subquery = ServerInbound.objects.filter(
|
||||
server_id=OuterRef('pk'),
|
||||
active=True,
|
||||
inbound__subscriptiongroup__usersubscription__active=True,
|
||||
inbound__subscriptiongroup__is_active=True
|
||||
).values('server_id').annotate(
|
||||
count=Count('inbound__subscriptiongroup__usersubscription__user', distinct=True)
|
||||
).values('count')
|
||||
|
||||
qs = qs.annotate(
|
||||
xray_user_count=Subquery(xray_user_count_subquery, output_field=IntegerField()),
|
||||
user_count=Case(
|
||||
When(server_type='xray_v2', then=F('xray_user_count')),
|
||||
default=F('acl_user_count'),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
)
|
||||
|
||||
# Handle None values from subquery
|
||||
qs = qs.annotate(
|
||||
user_count=Case(
|
||||
When(server_type='xray_v2', user_count__isnull=True, then=Value(0)),
|
||||
When(server_type='xray_v2', then=F('xray_user_count')),
|
||||
default=F('acl_user_count'),
|
||||
output_field=IntegerField()
|
||||
)
|
||||
)
|
||||
|
||||
qs = qs.prefetch_related(
|
||||
'acl_set__links',
|
||||
'acl_set__user'
|
||||
@@ -1015,6 +1135,50 @@ class UserAdmin(admin.ModelAdmin):
|
||||
for group in xray_groups:
|
||||
html += f'<li>{group}</li>'
|
||||
html += '</ul>'
|
||||
|
||||
# Try to get traffic statistics for this user
|
||||
try:
|
||||
from vpn.server_plugins.xray_v2 import XrayServerV2
|
||||
traffic_total_up = 0
|
||||
traffic_total_down = 0
|
||||
servers_checked = set()
|
||||
|
||||
# Get all Xray servers
|
||||
xray_servers = XrayServerV2.objects.filter(api_enabled=True, stats_enabled=True)
|
||||
|
||||
for server in xray_servers:
|
||||
if server.name not in servers_checked:
|
||||
try:
|
||||
from vpn.xray_api_v2.client import XrayClient
|
||||
from vpn.xray_api_v2.stats import StatsManager
|
||||
|
||||
client = XrayClient(server=server.api_address)
|
||||
stats_manager = StatsManager(client)
|
||||
|
||||
# Get user stats (use email format: username@servername)
|
||||
user_email = f"{obj.username}@{server.name}"
|
||||
user_stats = stats_manager.get_user_stats(user_email)
|
||||
|
||||
if user_stats:
|
||||
traffic_total_up += user_stats.get('uplink', 0)
|
||||
traffic_total_down += user_stats.get('downlink', 0)
|
||||
|
||||
servers_checked.add(server.name)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.debug(f"Could not get user stats from server {server.name}: {e}")
|
||||
|
||||
# Format traffic if we got any
|
||||
if traffic_total_up > 0 or traffic_total_down > 0:
|
||||
|
||||
html += f'<p style="margin: 10px 0 5px 0; color: #007cba;"><strong>📊 Traffic Statistics:</strong></p>'
|
||||
html += f'<p style="margin: 5px 0 5px 20px;">↑ Upload: <strong>{format_bytes(traffic_total_up)}</strong></p>'
|
||||
html += f'<p style="margin: 5px 0 5px 20px;">↓ Download: <strong>{format_bytes(traffic_total_down)}</strong></p>'
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.debug(f"Could not get traffic stats for user {obj.username}: {e}")
|
||||
else:
|
||||
html += '<p style="margin: 5px 0; color: #6c757d;">No Xray subscriptions</p>'
|
||||
html += '</div>'
|
||||
|
@@ -782,7 +782,7 @@ class XrayServerV2Admin(admin.ModelAdmin):
|
||||
list_display = ['name', 'client_hostname', 'api_address', 'api_enabled', 'stats_enabled', 'registration_date']
|
||||
list_filter = ['api_enabled', 'stats_enabled', 'registration_date']
|
||||
search_fields = ['name', 'client_hostname', 'comment']
|
||||
readonly_fields = ['server_type', 'registration_date']
|
||||
readonly_fields = ['server_type', 'registration_date', 'traffic_statistics']
|
||||
inlines = [ServerInboundInline]
|
||||
|
||||
def has_module_permission(self, request):
|
||||
@@ -799,6 +799,10 @@ class XrayServerV2Admin(admin.ModelAdmin):
|
||||
('API Settings', {
|
||||
'fields': ('api_enabled', 'stats_enabled')
|
||||
}),
|
||||
('Traffic Statistics', {
|
||||
'fields': ('traffic_statistics',),
|
||||
'description': 'Real-time traffic statistics from Xray server'
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('registration_date',),
|
||||
'classes': ('collapse',)
|
||||
@@ -826,3 +830,118 @@ class XrayServerV2Admin(admin.ModelAdmin):
|
||||
statuses.append(f"{server.name}: {status.get('accessible', 'Unknown')}")
|
||||
self.message_user(request, f"Server statuses: {', '.join(statuses)}")
|
||||
get_status.short_description = "Check status of selected servers"
|
||||
|
||||
def traffic_statistics(self, obj):
|
||||
"""Display traffic statistics for this server"""
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.html import format_html
|
||||
|
||||
if not obj.pk:
|
||||
return "Save server first to see statistics"
|
||||
|
||||
if not obj.api_enabled or not obj.stats_enabled:
|
||||
return "Statistics are disabled. Enable API and stats to see traffic data."
|
||||
|
||||
try:
|
||||
from vpn.xray_api_v2.client import XrayClient
|
||||
from vpn.xray_api_v2.stats import StatsManager
|
||||
|
||||
client = XrayClient(server=obj.api_address)
|
||||
stats_manager = StatsManager(client)
|
||||
|
||||
# Get traffic summary
|
||||
traffic_summary = stats_manager.get_traffic_summary()
|
||||
|
||||
# Format bytes
|
||||
def format_bytes(bytes_val):
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if bytes_val < 1024.0:
|
||||
return f"{bytes_val:.1f}{unit}"
|
||||
bytes_val /= 1024.0
|
||||
return f"{bytes_val:.1f}PB"
|
||||
|
||||
html = '<div style="background: #f8f9fa; padding: 15px; border-radius: 5px;">'
|
||||
|
||||
# User statistics
|
||||
users = traffic_summary.get('users', {})
|
||||
if users:
|
||||
html += '<h4 style="margin: 0 0 15px 0; color: #495057;">👥 User Traffic</h4>'
|
||||
html += '<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">'
|
||||
html += '<thead><tr style="background: #e9ecef;">'
|
||||
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">User</th>'
|
||||
html += '<th style="padding: 8px; text-align: right; border: 1px solid #dee2e6;">Upload</th>'
|
||||
html += '<th style="padding: 8px; text-align: right; border: 1px solid #dee2e6;">Download</th>'
|
||||
html += '<th style="padding: 8px; text-align: right; border: 1px solid #dee2e6;">Total</th>'
|
||||
html += '</tr></thead><tbody>'
|
||||
|
||||
# Sort users by total traffic
|
||||
sorted_users = sorted(users.items(),
|
||||
key=lambda x: x[1].get('uplink', 0) + x[1].get('downlink', 0),
|
||||
reverse=True)
|
||||
|
||||
total_up = 0
|
||||
total_down = 0
|
||||
|
||||
for email, stats in sorted_users[:20]: # Show top 20 users
|
||||
up = stats.get('uplink', 0)
|
||||
down = stats.get('downlink', 0)
|
||||
total = up + down
|
||||
total_up += up
|
||||
total_down += down
|
||||
|
||||
html += '<tr>'
|
||||
html += f'<td style="padding: 6px; border: 1px solid #dee2e6;">{email}</td>'
|
||||
html += f'<td style="padding: 6px; text-align: right; border: 1px solid #dee2e6;">↑ {format_bytes(up)}</td>'
|
||||
html += f'<td style="padding: 6px; text-align: right; border: 1px solid #dee2e6;">↓ {format_bytes(down)}</td>'
|
||||
html += f'<td style="padding: 6px; text-align: right; border: 1px solid #dee2e6; font-weight: bold;">{format_bytes(total)}</td>'
|
||||
html += '</tr>'
|
||||
|
||||
if len(users) > 20:
|
||||
html += f'<tr><td colspan="4" style="padding: 6px; text-align: center; border: 1px solid #dee2e6; color: #6c757d;">... and {len(users) - 20} more users</td></tr>'
|
||||
|
||||
# Total row
|
||||
html += '<tr style="background: #e9ecef; font-weight: bold;">'
|
||||
html += f'<td style="padding: 8px; border: 1px solid #dee2e6;">Total ({len(users)} users)</td>'
|
||||
html += f'<td style="padding: 8px; text-align: right; border: 1px solid #dee2e6;">↑ {format_bytes(total_up)}</td>'
|
||||
html += f'<td style="padding: 8px; text-align: right; border: 1px solid #dee2e6;">↓ {format_bytes(total_down)}</td>'
|
||||
html += f'<td style="padding: 8px; text-align: right; border: 1px solid #dee2e6;">{format_bytes(total_up + total_down)}</td>'
|
||||
html += '</tr>'
|
||||
|
||||
html += '</tbody></table>'
|
||||
else:
|
||||
html += '<p style="color: #6c757d;">No user traffic data available</p>'
|
||||
|
||||
# Inbound statistics
|
||||
inbounds = traffic_summary.get('inbounds', {})
|
||||
if inbounds:
|
||||
html += '<h4 style="margin: 20px 0 15px 0; color: #495057;">📡 Inbound Traffic</h4>'
|
||||
html += '<table style="width: 100%; border-collapse: collapse;">'
|
||||
html += '<thead><tr style="background: #e9ecef;">'
|
||||
html += '<th style="padding: 8px; text-align: left; border: 1px solid #dee2e6;">Inbound</th>'
|
||||
html += '<th style="padding: 8px; text-align: right; border: 1px solid #dee2e6;">Upload</th>'
|
||||
html += '<th style="padding: 8px; text-align: right; border: 1px solid #dee2e6;">Download</th>'
|
||||
html += '<th style="padding: 8px; text-align: right; border: 1px solid #dee2e6;">Total</th>'
|
||||
html += '</tr></thead><tbody>'
|
||||
|
||||
for tag, stats in inbounds.items():
|
||||
up = stats.get('uplink', 0)
|
||||
down = stats.get('downlink', 0)
|
||||
total = up + down
|
||||
|
||||
html += '<tr>'
|
||||
html += f'<td style="padding: 6px; border: 1px solid #dee2e6;">{tag}</td>'
|
||||
html += f'<td style="padding: 6px; text-align: right; border: 1px solid #dee2e6;">↑ {format_bytes(up)}</td>'
|
||||
html += f'<td style="padding: 6px; text-align: right; border: 1px solid #dee2e6;">↓ {format_bytes(down)}</td>'
|
||||
html += f'<td style="padding: 6px; text-align: right; border: 1px solid #dee2e6; font-weight: bold;">{format_bytes(total)}</td>'
|
||||
html += '</tr>'
|
||||
|
||||
html += '</tbody></table>'
|
||||
|
||||
html += '</div>'
|
||||
|
||||
return format_html(html)
|
||||
|
||||
except Exception as e:
|
||||
return f"Error fetching statistics: {str(e)}"
|
||||
|
||||
traffic_statistics.short_description = 'Traffic Statistics'
|
Reference in New Issue
Block a user