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:
@@ -333,3 +333,10 @@ table.changelist-results td:nth-child(6) {
|
|||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide xray-subscriptions tab if it appears */
|
||||||
|
#xray-subscriptions-tab,
|
||||||
|
a[href="#xray-subscriptions-tab"],
|
||||||
|
li:has(a[href="#xray-subscriptions-tab"]) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
132
vpn/admin.py
132
vpn/admin.py
@@ -928,12 +928,38 @@ class ServerAdmin(PolymorphicParentModelAdmin):
|
|||||||
return redirect('admin:vpn_server_changelist')
|
return redirect('admin:vpn_server_changelist')
|
||||||
|
|
||||||
#admin.site.register(User, UserAdmin)
|
#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)
|
@admin.register(User)
|
||||||
class UserAdmin(admin.ModelAdmin):
|
class UserAdmin(admin.ModelAdmin):
|
||||||
form = UserForm
|
form = UserForm
|
||||||
list_display = ('username', 'comment', 'registration_date', 'hash_link', 'server_count')
|
list_display = ('username', 'comment', 'registration_date', 'hash_link', 'server_count')
|
||||||
search_fields = ('username', 'hash')
|
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:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
@@ -945,7 +971,7 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
'fields': ('username', 'first_name', 'last_name', 'email', 'comment')
|
'fields': ('username', 'first_name', 'last_name', 'email', 'comment')
|
||||||
}),
|
}),
|
||||||
('Access Information', {
|
('Access Information', {
|
||||||
'fields': ('hash_link', 'is_active')
|
'fields': ('hash_link', 'is_active', 'vpn_access_summary')
|
||||||
}),
|
}),
|
||||||
('Statistics & Server Management', {
|
('Statistics & Server Management', {
|
||||||
'fields': ('user_statistics_summary',),
|
'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')
|
@admin.display(description='User Portal', ordering='hash')
|
||||||
def hash_link(self, obj):
|
def hash_link(self, obj):
|
||||||
portal_url = f"{EXTERNAL_ADDRESS}/u/{obj.hash}"
|
portal_url = f"{EXTERNAL_ADDRESS}/u/{obj.hash}"
|
||||||
@@ -1016,9 +1086,22 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
links = list(acl.links.all())
|
links = list(acl.links.all())
|
||||||
|
|
||||||
# Server header (no slow server status checks)
|
# 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'<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 stats
|
||||||
server_stat = next((s for s in server_breakdown if s['server_name'] == server.name), None)
|
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 += '<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;">'
|
html += '<div style="display: flex; gap: 8px; flex-wrap: wrap;">'
|
||||||
for server in unassigned_servers:
|
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'<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'data-server-id="{server.id}" data-server-name="{server.name}" '
|
||||||
html += f'{type_icon} {server.name}'
|
html += f'title="{type_label} server">'
|
||||||
|
html += f'{type_icon} {server.name} ({type_label})'
|
||||||
html += f'</button>'
|
html += f'</button>'
|
||||||
html += '</div></div>'
|
html += '</div></div>'
|
||||||
|
|
||||||
@@ -1305,24 +1402,9 @@ class UserAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
return super().change_view(request, object_id, form_url, extra_context)
|
return super().change_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
# Removed save_model as we no longer manage servers directly through the form
|
||||||
import logging
|
# Legacy VPN access is now managed through the ACL admin interface
|
||||||
logger = logging.getLogger(__name__)
|
# Xray access is managed through the UserXraySubscriptionInline
|
||||||
|
|
||||||
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}")
|
|
||||||
# Note: get_or_create will use the default save() method which creates default links
|
# Note: get_or_create will use the default save() method which creates default links
|
||||||
|
|
||||||
@admin.register(AccessLog)
|
@admin.register(AccessLog)
|
||||||
|
@@ -592,80 +592,12 @@ class UserSubscriptionInline(admin.TabularInline):
|
|||||||
def add_subscription_management_to_user(UserAdmin):
|
def add_subscription_management_to_user(UserAdmin):
|
||||||
"""Add subscription management to existing User admin"""
|
"""Add subscription management to existing User admin"""
|
||||||
|
|
||||||
# Add inline
|
# Add inline only - no fieldset or widget
|
||||||
if hasattr(UserAdmin, 'inlines'):
|
if hasattr(UserAdmin, 'inlines'):
|
||||||
UserAdmin.inlines = list(UserAdmin.inlines) + [UserSubscriptionInline]
|
UserAdmin.inlines = list(UserAdmin.inlines) + [UserSubscriptionInline]
|
||||||
else:
|
else:
|
||||||
UserAdmin.inlines = [UserSubscriptionInline]
|
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
|
# UserSubscription admin will be integrated into unified Subscriptions admin
|
||||||
class UserSubscriptionAdmin(admin.ModelAdmin):
|
class UserSubscriptionAdmin(admin.ModelAdmin):
|
||||||
|
@@ -1,14 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from .models import User
|
from .models import User
|
||||||
from .server_plugins import Server
|
|
||||||
|
|
||||||
class UserForm(forms.ModelForm):
|
class UserForm(forms.ModelForm):
|
||||||
servers = forms.ModelMultipleChoiceField(
|
|
||||||
queryset=Server.objects.all(),
|
|
||||||
widget=forms.CheckboxSelectMultiple,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
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
|
# Return with proper headers for subscription
|
||||||
response = HttpResponse(subscription_b64, content_type="text/plain; charset=utf-8")
|
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'
|
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
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
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
|
# VLESS URL format: vless://uuid@host:port?params#name
|
||||||
params = []
|
params = []
|
||||||
|
|
||||||
if inbound.network != 'tcp':
|
# Always add transport type for VLESS
|
||||||
params.append(f"type={inbound.network}")
|
params.append(f"type={inbound.network}")
|
||||||
|
|
||||||
if inbound.security != 'none':
|
if inbound.security != 'none':
|
||||||
params.append(f"security={inbound.security}")
|
params.append(f"security={inbound.security}")
|
||||||
|
Reference in New Issue
Block a user