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:
132
vpn/admin.py
132
vpn/admin.py
@@ -928,12 +928,38 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
||||
return redirect('admin:vpn_server_changelist')
|
||||
|
||||
#admin.site.register(User, UserAdmin)
|
||||
# Inline for legacy VPN access (Outline/Wireguard)
|
||||
class UserACLInline(admin.TabularInline):
|
||||
model = ACL
|
||||
extra = 0
|
||||
fields = ('server', 'created_at', 'link_count')
|
||||
readonly_fields = ('created_at', 'link_count')
|
||||
verbose_name = "Legacy VPN Server Access"
|
||||
verbose_name_plural = "Legacy VPN Server Access (Outline/Wireguard)"
|
||||
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
if db_field.name == "server":
|
||||
# Only show old-style servers (Outline/Wireguard)
|
||||
kwargs["queryset"] = Server.objects.exclude(server_type='xray_v2')
|
||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
@admin.display(description='Links')
|
||||
def link_count(self, obj):
|
||||
count = obj.links.count()
|
||||
return format_html(
|
||||
'<span style="font-weight: bold;">{}</span> link(s)',
|
||||
count
|
||||
)
|
||||
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
form = UserForm
|
||||
list_display = ('username', 'comment', 'registration_date', 'hash_link', 'server_count')
|
||||
search_fields = ('username', 'hash')
|
||||
readonly_fields = ('hash_link', 'user_statistics_summary')
|
||||
readonly_fields = ('hash_link', 'vpn_access_summary', 'user_statistics_summary')
|
||||
inlines = [] # All VPN access info is now in vpn_access_summary
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
@@ -945,7 +971,7 @@ class UserAdmin(admin.ModelAdmin):
|
||||
'fields': ('username', 'first_name', 'last_name', 'email', 'comment')
|
||||
}),
|
||||
('Access Information', {
|
||||
'fields': ('hash_link', 'is_active')
|
||||
'fields': ('hash_link', 'is_active', 'vpn_access_summary')
|
||||
}),
|
||||
('Statistics & Server Management', {
|
||||
'fields': ('user_statistics_summary',),
|
||||
@@ -953,6 +979,50 @@ class UserAdmin(admin.ModelAdmin):
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.display(description='VPN Access Summary')
|
||||
def vpn_access_summary(self, obj):
|
||||
"""Display summary of user's VPN access"""
|
||||
if not obj.pk:
|
||||
return "Save user first to see VPN access"
|
||||
|
||||
# Get legacy VPN access
|
||||
acl_count = ACL.objects.filter(user=obj).count()
|
||||
legacy_links = ACLLink.objects.filter(acl__user=obj).count()
|
||||
|
||||
# Get Xray access
|
||||
from vpn.models_xray import UserSubscription
|
||||
xray_subs = UserSubscription.objects.filter(user=obj, active=True).select_related('subscription_group')
|
||||
xray_groups = [sub.subscription_group.name for sub in xray_subs]
|
||||
|
||||
html = '<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 10px 0;">'
|
||||
|
||||
# Legacy VPN section
|
||||
html += '<div style="margin-bottom: 15px;">'
|
||||
html += '<h4 style="margin: 0 0 10px 0; color: #495057;">📡 Legacy VPN (Outline/Wireguard)</h4>'
|
||||
if acl_count > 0:
|
||||
html += f'<p style="margin: 5px 0;">✅ Access to <strong>{acl_count}</strong> server(s)</p>'
|
||||
html += f'<p style="margin: 5px 0;">🔗 Total links: <strong>{legacy_links}</strong></p>'
|
||||
else:
|
||||
html += '<p style="margin: 5px 0; color: #6c757d;">No legacy VPN access</p>'
|
||||
html += '</div>'
|
||||
|
||||
# Xray section
|
||||
html += '<div>'
|
||||
html += '<h4 style="margin: 0 0 10px 0; color: #495057;">🚀 Xray VPN</h4>'
|
||||
if xray_groups:
|
||||
html += f'<p style="margin: 5px 0;">✅ Active subscriptions: <strong>{len(xray_groups)}</strong></p>'
|
||||
html += '<ul style="margin: 5px 0; padding-left: 20px;">'
|
||||
for group in xray_groups:
|
||||
html += f'<li>{group}</li>'
|
||||
html += '</ul>'
|
||||
else:
|
||||
html += '<p style="margin: 5px 0; color: #6c757d;">No Xray subscriptions</p>'
|
||||
html += '</div>'
|
||||
|
||||
html += '</div>'
|
||||
|
||||
return format_html(html)
|
||||
|
||||
@admin.display(description='User Portal', ordering='hash')
|
||||
def hash_link(self, obj):
|
||||
portal_url = f"{EXTERNAL_ADDRESS}/u/{obj.hash}"
|
||||
@@ -1016,9 +1086,22 @@ class UserAdmin(admin.ModelAdmin):
|
||||
links = list(acl.links.all())
|
||||
|
||||
# Server header (no slow server status checks)
|
||||
type_icon = '🔵' if server.server_type == 'outline' else '🟢' if server.server_type == 'wireguard' else '🟣' if server.server_type == 'xray_core' else ''
|
||||
# Determine server type icon and label
|
||||
if server.server_type == 'Outline':
|
||||
type_icon = '🔵'
|
||||
type_label = 'Outline'
|
||||
elif server.server_type == 'Wireguard':
|
||||
type_icon = '🟢'
|
||||
type_label = 'Wireguard'
|
||||
elif server.server_type in ['xray_core', 'xray_v2']:
|
||||
type_icon = '🟣'
|
||||
type_label = 'Xray'
|
||||
else:
|
||||
type_icon = '❓'
|
||||
type_label = server.server_type
|
||||
|
||||
html += f'<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">'
|
||||
html += f'<h5 style="margin: 0; color: #333; font-size: 14px; font-weight: 600;">{type_icon} {server.name}</h5>'
|
||||
html += f'<h5 style="margin: 0; color: #333; font-size: 14px; font-weight: 600;">{type_icon} {server.name} ({type_label})</h5>'
|
||||
|
||||
# Server stats
|
||||
server_stat = next((s for s in server_breakdown if s['server_name'] == server.name), None)
|
||||
@@ -1091,10 +1174,24 @@ class UserAdmin(admin.ModelAdmin):
|
||||
html += '<h5 style="margin: 0 0 8px 0; color: #0c5460; font-size: 13px;">➕ Available Servers</h5>'
|
||||
html += '<div style="display: flex; gap: 8px; flex-wrap: wrap;">'
|
||||
for server in unassigned_servers:
|
||||
type_icon = '🔵' if server.server_type == 'outline' else '🟢' if server.server_type == 'wireguard' else '🟣' if server.server_type == 'xray_core' else ''
|
||||
# Determine server type icon and label
|
||||
if server.server_type == 'Outline':
|
||||
type_icon = '🔵'
|
||||
type_label = 'Outline'
|
||||
elif server.server_type == 'Wireguard':
|
||||
type_icon = '🟢'
|
||||
type_label = 'Wireguard'
|
||||
elif server.server_type in ['xray_core', 'xray_v2']:
|
||||
type_icon = '🟣'
|
||||
type_label = 'Xray'
|
||||
else:
|
||||
type_icon = '❓'
|
||||
type_label = server.server_type
|
||||
|
||||
html += f'<button type="button" class="btn btn-sm btn-outline-info btn-sm-custom add-server-btn" '
|
||||
html += f'data-server-id="{server.id}" data-server-name="{server.name}">'
|
||||
html += f'{type_icon} {server.name}'
|
||||
html += f'data-server-id="{server.id}" data-server-name="{server.name}" '
|
||||
html += f'title="{type_label} server">'
|
||||
html += f'{type_icon} {server.name} ({type_label})'
|
||||
html += f'</button>'
|
||||
html += '</div></div>'
|
||||
|
||||
@@ -1305,24 +1402,9 @@ class UserAdmin(admin.ModelAdmin):
|
||||
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
super().save_model(request, obj, form, change)
|
||||
selected_servers = form.cleaned_data.get('servers', [])
|
||||
|
||||
# Remove ACLs that are no longer selected
|
||||
removed_acls = ACL.objects.filter(user=obj).exclude(server__in=selected_servers)
|
||||
for acl in removed_acls:
|
||||
logger.info(f"Removing ACL for user {obj.username} from server {acl.server.name}")
|
||||
removed_acls.delete()
|
||||
|
||||
# Create new ACLs for newly selected servers (with default links)
|
||||
for server in selected_servers:
|
||||
acl, created = ACL.objects.get_or_create(user=obj, server=server)
|
||||
if created:
|
||||
logger.info(f"Created new ACL for user {obj.username} on server {server.name}")
|
||||
# Removed save_model as we no longer manage servers directly through the form
|
||||
# Legacy VPN access is now managed through the ACL admin interface
|
||||
# Xray access is managed through the UserXraySubscriptionInline
|
||||
# Note: get_or_create will use the default save() method which creates default links
|
||||
|
||||
@admin.register(AccessLog)
|
||||
|
@@ -592,79 +592,11 @@ class UserSubscriptionInline(admin.TabularInline):
|
||||
def add_subscription_management_to_user(UserAdmin):
|
||||
"""Add subscription management to existing User admin"""
|
||||
|
||||
# Add inline
|
||||
# Add inline only - no fieldset or widget
|
||||
if hasattr(UserAdmin, 'inlines'):
|
||||
UserAdmin.inlines = list(UserAdmin.inlines) + [UserSubscriptionInline]
|
||||
else:
|
||||
UserAdmin.inlines = [UserSubscriptionInline]
|
||||
|
||||
# Add custom fields to fieldsets
|
||||
original_fieldsets = list(UserAdmin.fieldsets)
|
||||
|
||||
# Find where to insert our fieldset
|
||||
insert_index = len(original_fieldsets)
|
||||
for i, (title, fields_dict) in enumerate(original_fieldsets):
|
||||
if title and 'Statistics' in title:
|
||||
insert_index = i + 1
|
||||
break
|
||||
|
||||
# Insert our fieldset
|
||||
subscription_fieldset = (
|
||||
'Xray Subscriptions', {
|
||||
'fields': ('subscription_groups_widget',),
|
||||
'classes': ('wide',)
|
||||
}
|
||||
)
|
||||
original_fieldsets.insert(insert_index, subscription_fieldset)
|
||||
UserAdmin.fieldsets = tuple(original_fieldsets)
|
||||
|
||||
# Add readonly field
|
||||
if hasattr(UserAdmin, 'readonly_fields'):
|
||||
UserAdmin.readonly_fields = list(UserAdmin.readonly_fields) + ['subscription_groups_widget']
|
||||
else:
|
||||
UserAdmin.readonly_fields = ['subscription_groups_widget']
|
||||
|
||||
# Add method for displaying subscription groups
|
||||
def subscription_groups_widget(self, obj):
|
||||
"""Display subscription groups management widget"""
|
||||
if not obj or not obj.pk:
|
||||
return mark_safe('<div style="color: #6c757d;">Save user first to manage subscriptions</div>')
|
||||
|
||||
# Get all groups and user's current subscriptions
|
||||
all_groups = SubscriptionGroup.objects.filter(is_active=True)
|
||||
user_groups = obj.xray_subscriptions.filter(active=True).values_list('subscription_group_id', flat=True)
|
||||
|
||||
html = '<div style="background: #f8f9fa; padding: 15px; border-radius: 4px;">'
|
||||
html += '<h4 style="margin-top: 0;">Available Subscription Groups:</h4>'
|
||||
|
||||
if all_groups:
|
||||
html += '<div style="display: grid; gap: 10px;">'
|
||||
for group in all_groups:
|
||||
checked = 'checked' if group.id in user_groups else ''
|
||||
status = '✅' if group.id in user_groups else '⬜'
|
||||
|
||||
html += f'''
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 8px; background: white; border-radius: 4px;">
|
||||
<span style="font-size: 18px;">{status}</span>
|
||||
<label style="flex: 1; cursor: pointer;">
|
||||
<strong>{group.name}</strong>
|
||||
{f' - {group.description}' if group.description else ''}
|
||||
<small style="color: #6c757d;"> ({group.inbound_count} inbounds)</small>
|
||||
</label>
|
||||
</div>
|
||||
'''
|
||||
html += '</div>'
|
||||
html += '<div style="margin-top: 10px; color: #6c757d; font-size: 12px;">'
|
||||
html += 'ℹ️ Use the inline form below to manage subscriptions'
|
||||
html += '</div>'
|
||||
else:
|
||||
html += '<div style="color: #6c757d;">No active subscription groups available</div>'
|
||||
|
||||
html += '</div>'
|
||||
return mark_safe(html)
|
||||
|
||||
subscription_groups_widget.short_description = 'Subscription Groups Overview'
|
||||
UserAdmin.subscription_groups_widget = subscription_groups_widget
|
||||
|
||||
|
||||
# UserSubscription admin will be integrated into unified Subscriptions admin
|
||||
|
@@ -1,14 +1,7 @@
|
||||
from django import forms
|
||||
from .models import User
|
||||
from .server_plugins import Server
|
||||
|
||||
class UserForm(forms.ModelForm):
|
||||
servers = forms.ModelMultipleChoiceField(
|
||||
queryset=Server.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'comment', 'servers']
|
||||
fields = ['username', 'first_name', 'last_name', 'email', 'comment', 'is_active']
|
||||
|
21
vpn/views.py
21
vpn/views.py
@@ -438,8 +438,23 @@ def xray_subscription(request, user_hash):
|
||||
|
||||
# Return with proper headers for subscription
|
||||
response = HttpResponse(subscription_b64, content_type="text/plain; charset=utf-8")
|
||||
response['Content-Disposition'] = f'attachment; filename="{user.username}_xray_subscription.txt"'
|
||||
response['Content-Disposition'] = f'attachment; filename="{user.username}"'
|
||||
response['Cache-Control'] = 'no-cache'
|
||||
|
||||
# Add subscription-specific headers like other providers
|
||||
import base64 as b64
|
||||
profile_title_b64 = b64.b64encode("OutFleet VPN".encode('utf-8')).decode('utf-8')
|
||||
response['profile-title'] = f'base64:{profile_title_b64}'
|
||||
response['profile-update-interval'] = '24' # Update every 24 hours
|
||||
response['profile-web-page-url'] = f'https://{request.get_host()}/u/{user_hash}'
|
||||
response['support-url'] = f'https://{request.get_host()}/admin/'
|
||||
|
||||
# Add user info without limits (unlimited service)
|
||||
# Set very high limits to indicate "unlimited"
|
||||
import time
|
||||
expire_timestamp = int(time.time()) + (365 * 24 * 60 * 60) # 1 year from now
|
||||
response['subscription-userinfo'] = f'upload=0; download=0; total=1099511627776; expire={expire_timestamp}'
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
@@ -540,8 +555,8 @@ def generate_xray_connection_string(user, inbound, server_name=None, server_host
|
||||
# VLESS URL format: vless://uuid@host:port?params#name
|
||||
params = []
|
||||
|
||||
if inbound.network != 'tcp':
|
||||
params.append(f"type={inbound.network}")
|
||||
# Always add transport type for VLESS
|
||||
params.append(f"type={inbound.network}")
|
||||
|
||||
if inbound.security != 'none':
|
||||
params.append(f"security={inbound.security}")
|
||||
|
Reference in New Issue
Block a user