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 vpn.forms import UserForm
|
||||||
from mysite.settings import EXTERNAL_ADDRESS
|
from mysite.settings import EXTERNAL_ADDRESS
|
||||||
from django.db.models import Max, Subquery, OuterRef, Q
|
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 (
|
from .server_plugins import (
|
||||||
Server,
|
Server,
|
||||||
WireguardServer,
|
WireguardServer,
|
||||||
@@ -802,7 +811,25 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
|
|
||||||
user_count = obj.user_count if hasattr(obj, 'user_count') else 0
|
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'):
|
if hasattr(obj, 'acl_set'):
|
||||||
all_links = []
|
all_links = []
|
||||||
for acl in obj.acl_set.all():
|
for acl in obj.acl_set.all():
|
||||||
@@ -829,14 +856,65 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
if user_count == 0:
|
if user_count == 0:
|
||||||
color = '#9ca3af' # gray - no users
|
color = '#9ca3af' # gray - no users
|
||||||
elif total_links == 0:
|
elif total_links == 0:
|
||||||
color = '#dc2626' # red - no links
|
color = '#dc2626' # red - no links/inbounds
|
||||||
elif total_links > 0 and active_links > total_links * 0.7: # High activity
|
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
|
color = '#16a34a' # green
|
||||||
elif total_links > 0 and active_links > total_links * 0.3: # Medium activity
|
elif total_links > 0 and active_links > total_links * 0.3: # Medium activity
|
||||||
color = '#eab308' # yellow
|
color = '#eab308' # yellow
|
||||||
else:
|
else:
|
||||||
color = '#f97316' # orange - low activity
|
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(
|
return mark_safe(
|
||||||
f'<div style="font-size: 12px;">' +
|
f'<div style="font-size: 12px;">' +
|
||||||
f'<div style="color: {color}; font-weight: bold;">👥 {user_count} users</div>' +
|
f'<div style="color: {color}; font-weight: bold;">👥 {user_count} users</div>' +
|
||||||
@@ -844,6 +922,10 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
f'</div>'
|
f'</div>'
|
||||||
)
|
)
|
||||||
except Exception as e:
|
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>')
|
return mark_safe(f'<span style="color: #dc2626; font-size: 11px;">Stats error: {e}</span>')
|
||||||
|
|
||||||
@admin.display(description='Activity')
|
@admin.display(description='Activity')
|
||||||
@@ -896,8 +978,46 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
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 = 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(
|
qs = qs.prefetch_related(
|
||||||
'acl_set__links',
|
'acl_set__links',
|
||||||
'acl_set__user'
|
'acl_set__user'
|
||||||
@@ -1015,6 +1135,50 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
for group in xray_groups:
|
for group in xray_groups:
|
||||||
html += f'<li>{group}</li>'
|
html += f'<li>{group}</li>'
|
||||||
html += '</ul>'
|
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:
|
else:
|
||||||
html += '<p style="margin: 5px 0; color: #6c757d;">No Xray subscriptions</p>'
|
html += '<p style="margin: 5px 0; color: #6c757d;">No Xray subscriptions</p>'
|
||||||
html += '</div>'
|
html += '</div>'
|
||||||
|
@@ -782,7 +782,7 @@ class XrayServerV2Admin(admin.ModelAdmin):
|
|||||||
list_display = ['name', 'client_hostname', 'api_address', 'api_enabled', 'stats_enabled', 'registration_date']
|
list_display = ['name', 'client_hostname', 'api_address', 'api_enabled', 'stats_enabled', 'registration_date']
|
||||||
list_filter = ['api_enabled', 'stats_enabled', 'registration_date']
|
list_filter = ['api_enabled', 'stats_enabled', 'registration_date']
|
||||||
search_fields = ['name', 'client_hostname', 'comment']
|
search_fields = ['name', 'client_hostname', 'comment']
|
||||||
readonly_fields = ['server_type', 'registration_date']
|
readonly_fields = ['server_type', 'registration_date', 'traffic_statistics']
|
||||||
inlines = [ServerInboundInline]
|
inlines = [ServerInboundInline]
|
||||||
|
|
||||||
def has_module_permission(self, request):
|
def has_module_permission(self, request):
|
||||||
@@ -799,6 +799,10 @@ class XrayServerV2Admin(admin.ModelAdmin):
|
|||||||
('API Settings', {
|
('API Settings', {
|
||||||
'fields': ('api_enabled', 'stats_enabled')
|
'fields': ('api_enabled', 'stats_enabled')
|
||||||
}),
|
}),
|
||||||
|
('Traffic Statistics', {
|
||||||
|
'fields': ('traffic_statistics',),
|
||||||
|
'description': 'Real-time traffic statistics from Xray server'
|
||||||
|
}),
|
||||||
('Timestamps', {
|
('Timestamps', {
|
||||||
'fields': ('registration_date',),
|
'fields': ('registration_date',),
|
||||||
'classes': ('collapse',)
|
'classes': ('collapse',)
|
||||||
@@ -826,3 +830,118 @@ class XrayServerV2Admin(admin.ModelAdmin):
|
|||||||
statuses.append(f"{server.name}: {status.get('accessible', 'Unknown')}")
|
statuses.append(f"{server.name}: {status.get('accessible', 'Unknown')}")
|
||||||
self.message_user(request, f"Server statuses: {', '.join(statuses)}")
|
self.message_user(request, f"Server statuses: {', '.join(statuses)}")
|
||||||
get_status.short_description = "Check status of selected servers"
|
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