mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-08-21 14:37:16 +00:00
Xray works.
This commit is contained in:
@@ -441,17 +441,18 @@ class CertificateAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Inbound)
|
@admin.register(Inbound)
|
||||||
class InboundAdmin(admin.ModelAdmin):
|
class InboundAdmin(admin.ModelAdmin):
|
||||||
"""Admin for inbound management"""
|
"""Admin for inbound template management"""
|
||||||
list_display = (
|
list_display = (
|
||||||
'name', 'protocol', 'port', 'network',
|
'name', 'protocol', 'port', 'network',
|
||||||
'security', 'certificate_status', 'group_count'
|
'security', 'certificate_status', 'group_count'
|
||||||
)
|
)
|
||||||
list_filter = ('protocol', 'network', 'security')
|
list_filter = ('protocol', 'network', 'security')
|
||||||
search_fields = ('name', 'domain')
|
search_fields = ('name',)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Basic Configuration', {
|
('Basic Configuration', {
|
||||||
'fields': ('name', 'protocol', 'port', 'domain')
|
'fields': ('name', 'protocol', 'port'),
|
||||||
|
'description': 'Domain will be taken from server client_hostname when deployed'
|
||||||
}),
|
}),
|
||||||
('Transport & Security', {
|
('Transport & Security', {
|
||||||
'fields': ('network', 'security', 'certificate', 'listen_address')
|
'fields': ('network', 'security', 'certificate', 'listen_address')
|
||||||
@@ -523,7 +524,6 @@ class InboundInline(admin.TabularInline):
|
|||||||
verbose_name_plural = "Inbounds in this group"
|
verbose_name_plural = "Inbounds in this group"
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SubscriptionGroup)
|
|
||||||
class SubscriptionGroupAdmin(admin.ModelAdmin):
|
class SubscriptionGroupAdmin(admin.ModelAdmin):
|
||||||
"""Admin for subscription groups"""
|
"""Admin for subscription groups"""
|
||||||
list_display = ('name', 'is_active', 'inbound_count', 'user_count', 'created_at')
|
list_display = ('name', 'is_active', 'inbound_count', 'user_count', 'created_at')
|
||||||
@@ -584,6 +584,7 @@ class SubscriptionGroupAdmin(admin.ModelAdmin):
|
|||||||
group_statistics.short_description = 'Group Statistics'
|
group_statistics.short_description = 'Group Statistics'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class UserSubscriptionInline(admin.TabularInline):
|
class UserSubscriptionInline(admin.TabularInline):
|
||||||
"""Inline for user subscriptions"""
|
"""Inline for user subscriptions"""
|
||||||
model = UserSubscription
|
model = UserSubscription
|
||||||
@@ -673,21 +674,19 @@ def add_subscription_management_to_user(UserAdmin):
|
|||||||
UserAdmin.subscription_groups_widget = subscription_groups_widget
|
UserAdmin.subscription_groups_widget = subscription_groups_widget
|
||||||
|
|
||||||
|
|
||||||
# Register admin for UserSubscription (if needed separately)
|
# UserSubscription admin will be integrated into unified Subscriptions admin
|
||||||
@admin.register(UserSubscription)
|
|
||||||
class UserSubscriptionAdmin(admin.ModelAdmin):
|
class UserSubscriptionAdmin(admin.ModelAdmin):
|
||||||
"""Standalone admin for user subscriptions"""
|
"""Admin for user subscriptions (integrated into unified Subscriptions admin)"""
|
||||||
list_display = ('user', 'subscription_group', 'active', 'created_at')
|
list_display = ('user', 'subscription_group', 'active', 'created_at')
|
||||||
list_filter = ('active', 'subscription_group')
|
list_filter = ('active', 'subscription_group')
|
||||||
search_fields = ('user__username', 'subscription_group__name')
|
search_fields = ('user__username', 'subscription_group__name')
|
||||||
date_hierarchy = 'created_at'
|
date_hierarchy = 'created_at'
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
# Prefer managing through User admin
|
return True # Allow adding subscriptions
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ServerInbound)
|
# ServerInbound admin is integrated into XrayServerV2 admin, not shown in main menu
|
||||||
class ServerInboundAdmin(admin.ModelAdmin):
|
class ServerInboundAdmin(admin.ModelAdmin):
|
||||||
"""Admin for server-inbound deployment tracking"""
|
"""Admin for server-inbound deployment tracking"""
|
||||||
list_display = ('server', 'inbound', 'active', 'deployed_at', 'updated_at')
|
list_display = ('server', 'inbound', 'active', 'deployed_at', 'updated_at')
|
||||||
@@ -696,27 +695,173 @@ class ServerInboundAdmin(admin.ModelAdmin):
|
|||||||
date_hierarchy = 'deployed_at'
|
date_hierarchy = 'deployed_at'
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Deployment', {
|
('Template Deployment', {
|
||||||
'fields': ('server', 'inbound', 'active')
|
'fields': ('server', 'inbound', 'active')
|
||||||
}),
|
}),
|
||||||
('Configuration', {
|
|
||||||
'fields': ('deployment_config_display', 'deployment_config'),
|
|
||||||
'classes': ('collapse',)
|
|
||||||
}),
|
|
||||||
('Timestamps', {
|
('Timestamps', {
|
||||||
'fields': ('deployed_at', 'updated_at'),
|
'fields': ('deployed_at', 'updated_at'),
|
||||||
'classes': ('collapse',)
|
'classes': ('collapse',)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
readonly_fields = ('deployment_config_display', 'deployed_at', 'updated_at')
|
readonly_fields = ('deployed_at', 'updated_at')
|
||||||
|
|
||||||
|
|
||||||
|
# Unified Subscriptions Admin with tabs
|
||||||
|
@admin.register(SubscriptionGroup)
|
||||||
|
class UnifiedSubscriptionsAdmin(admin.ModelAdmin):
|
||||||
|
"""Unified admin for managing both Subscription Groups and User Subscriptions"""
|
||||||
|
|
||||||
def deployment_config_display(self, obj):
|
# Use SubscriptionGroup as the base model but provide access to UserSubscription via tabs
|
||||||
"""Display deployment config in formatted JSON"""
|
list_display = ('name', 'is_active', 'inbound_count', 'user_count', 'created_at')
|
||||||
if obj.deployment_config:
|
list_filter = ('is_active',)
|
||||||
return format_html(
|
search_fields = ('name', 'description')
|
||||||
'<pre style="background: #f4f4f4; padding: 10px; border-radius: 4px; max-height: 300px; overflow-y: auto;">{}</pre>',
|
filter_horizontal = ('inbounds',)
|
||||||
json.dumps(obj.deployment_config, indent=2)
|
|
||||||
|
def get_urls(self):
|
||||||
|
"""Add custom URLs for user subscriptions tab"""
|
||||||
|
urls = super().get_urls()
|
||||||
|
custom_urls = [
|
||||||
|
path('user-subscriptions/',
|
||||||
|
self.admin_site.admin_view(self.user_subscriptions_view),
|
||||||
|
name='vpn_usersubscription_changelist_tab'),
|
||||||
|
]
|
||||||
|
return custom_urls + urls
|
||||||
|
|
||||||
|
def user_subscriptions_view(self, request):
|
||||||
|
"""Redirect to user subscriptions with tab navigation"""
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
return redirect('/admin/vpn/usersubscription/')
|
||||||
|
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
"""Override changelist to add tab navigation"""
|
||||||
|
extra_context = extra_context or {}
|
||||||
|
extra_context.update({
|
||||||
|
'show_tab_navigation': True,
|
||||||
|
'current_tab': 'subscription_groups'
|
||||||
|
})
|
||||||
|
return super().changelist_view(request, extra_context)
|
||||||
|
|
||||||
|
def change_view(self, request, object_id, form_url='', extra_context=None):
|
||||||
|
"""Override change view to add tab navigation"""
|
||||||
|
extra_context = extra_context or {}
|
||||||
|
extra_context.update({
|
||||||
|
'show_tab_navigation': True,
|
||||||
|
'current_tab': 'subscription_groups'
|
||||||
|
})
|
||||||
|
return super().change_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
|
def add_view(self, request, form_url='', extra_context=None):
|
||||||
|
"""Override add view to add tab navigation"""
|
||||||
|
extra_context = extra_context or {}
|
||||||
|
extra_context.update({
|
||||||
|
'show_tab_navigation': True,
|
||||||
|
'current_tab': 'subscription_groups'
|
||||||
|
})
|
||||||
|
return super().add_view(request, form_url, extra_context)
|
||||||
|
|
||||||
|
# Copy fieldsets and methods from SubscriptionGroupAdmin
|
||||||
|
fieldsets = (
|
||||||
|
('Group Information', {
|
||||||
|
'fields': ('name', 'description', 'is_active')
|
||||||
|
}),
|
||||||
|
('Inbounds', {
|
||||||
|
'fields': ('inbounds',),
|
||||||
|
'description': 'Select inbounds to include in this group. ' +
|
||||||
|
'<br><strong>🚀 Auto-sync enabled:</strong> Changes will be automatically deployed to servers!'
|
||||||
|
}),
|
||||||
|
('Statistics', {
|
||||||
|
'fields': ('group_statistics',),
|
||||||
|
'classes': ('collapse',)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly_fields = ('group_statistics',)
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
"""Override save to notify about auto-sync"""
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
if change:
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f'Subscription group "{obj.name}" updated. Changes will be automatically synchronized to all Xray servers.'
|
||||||
)
|
)
|
||||||
return 'No additional deployment configuration'
|
else:
|
||||||
deployment_config_display.short_description = 'Deployment Config Preview'
|
messages.success(
|
||||||
|
request,
|
||||||
|
f'Subscription group "{obj.name}" created. Inbounds will be automatically deployed when you add them to this group.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def group_statistics(self, obj):
|
||||||
|
"""Display group statistics"""
|
||||||
|
if obj.pk:
|
||||||
|
stats = {
|
||||||
|
'Total Inbounds': obj.inbound_count,
|
||||||
|
'Active Users': obj.user_count,
|
||||||
|
'Protocols': list(obj.inbounds.values_list('protocol', flat=True).distinct()),
|
||||||
|
'Ports': list(obj.inbounds.values_list('port', flat=True).distinct())
|
||||||
|
}
|
||||||
|
|
||||||
|
html = '<div style="background: #f4f4f4; padding: 10px; border-radius: 4px;">'
|
||||||
|
for key, value in stats.items():
|
||||||
|
if isinstance(value, list):
|
||||||
|
value = ', '.join(map(str, value))
|
||||||
|
html += f'<div><strong>{key}:</strong> {value}</div>'
|
||||||
|
html += '</div>'
|
||||||
|
|
||||||
|
return format_html(html)
|
||||||
|
return 'Save to see statistics'
|
||||||
|
group_statistics.short_description = 'Group Statistics'
|
||||||
|
|
||||||
|
|
||||||
|
# UserSubscription admin with tab navigation (hidden from main menu)
|
||||||
|
@admin.register(UserSubscription)
|
||||||
|
class UserSubscriptionTabAdmin(UserSubscriptionAdmin):
|
||||||
|
"""UserSubscription admin with tab navigation"""
|
||||||
|
|
||||||
|
def has_module_permission(self, request):
|
||||||
|
"""Hide this model from the main admin index"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_view_permission(self, request, obj=None):
|
||||||
|
"""Allow viewing through direct URL access"""
|
||||||
|
return request.user.is_staff
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
"""Allow adding through direct URL access"""
|
||||||
|
return request.user.is_staff
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
"""Allow changing through direct URL access"""
|
||||||
|
return request.user.is_staff
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
"""Allow deleting through direct URL access"""
|
||||||
|
return request.user.is_staff
|
||||||
|
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
"""Override changelist to add tab navigation"""
|
||||||
|
extra_context = extra_context or {}
|
||||||
|
extra_context.update({
|
||||||
|
'show_tab_navigation': True,
|
||||||
|
'current_tab': 'user_subscriptions'
|
||||||
|
})
|
||||||
|
return super().changelist_view(request, extra_context)
|
||||||
|
|
||||||
|
def change_view(self, request, object_id, form_url='', extra_context=None):
|
||||||
|
"""Override change view to add tab navigation"""
|
||||||
|
extra_context = extra_context or {}
|
||||||
|
extra_context.update({
|
||||||
|
'show_tab_navigation': True,
|
||||||
|
'current_tab': 'user_subscriptions'
|
||||||
|
})
|
||||||
|
return super().change_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
|
def add_view(self, request, form_url='', extra_context=None):
|
||||||
|
"""Override add view to add tab navigation"""
|
||||||
|
extra_context = extra_context or {}
|
||||||
|
extra_context.update({
|
||||||
|
'show_tab_navigation': True,
|
||||||
|
'current_tab': 'user_subscriptions'
|
||||||
|
})
|
||||||
|
return super().add_view(request, form_url, extra_context)
|
21
vpn/migrations/0022_remove_inbound_domain_field.py
Normal file
21
vpn/migrations/0022_remove_inbound_domain_field.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.1.7 on 2025-08-08 04:14
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('vpn', '0021_remove_xray_configuration'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='inbound',
|
||||||
|
options={'ordering': ['protocol', 'port'], 'verbose_name': 'Inbound Template', 'verbose_name_plural': 'Inbound Templates'},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='inbound',
|
||||||
|
name='domain',
|
||||||
|
),
|
||||||
|
]
|
17
vpn/migrations/0023_alter_subscriptiongroup_options.py
Normal file
17
vpn/migrations/0023_alter_subscriptiongroup_options.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.1.7 on 2025-08-08 04:32
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('vpn', '0022_remove_inbound_domain_field'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='subscriptiongroup',
|
||||||
|
options={'ordering': ['name'], 'verbose_name': 'Subscriptions', 'verbose_name_plural': 'Subscriptions'},
|
||||||
|
),
|
||||||
|
]
|
@@ -200,11 +200,6 @@ class Inbound(models.Model):
|
|||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
help_text="Certificate for TLS"
|
help_text="Certificate for TLS"
|
||||||
)
|
)
|
||||||
domain = models.CharField(
|
|
||||||
max_length=255,
|
|
||||||
blank=True,
|
|
||||||
help_text="Client connection domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Full configuration for Xray
|
# Full configuration for Xray
|
||||||
full_config = models.JSONField(
|
full_config = models.JSONField(
|
||||||
@@ -228,8 +223,8 @@ class Inbound(models.Model):
|
|||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Inbound"
|
verbose_name = "Inbound Template"
|
||||||
verbose_name_plural = "Inbounds"
|
verbose_name_plural = "Inbound Templates"
|
||||||
ordering = ['protocol', 'port']
|
ordering = ['protocol', 'port']
|
||||||
unique_together = [['port', 'listen_address']]
|
unique_together = [['port', 'listen_address']]
|
||||||
|
|
||||||
@@ -322,14 +317,14 @@ class Inbound(models.Model):
|
|||||||
elif self.network == "http":
|
elif self.network == "http":
|
||||||
stream_settings["httpSettings"] = {
|
stream_settings["httpSettings"] = {
|
||||||
"path": f"/{self.name}",
|
"path": f"/{self.name}",
|
||||||
"host": [self.domain] if self.domain else []
|
"host": [] # Will be filled when deployed to server
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add security settings
|
# Add security settings
|
||||||
if self.security == "tls":
|
if self.security == "tls":
|
||||||
stream_settings["security"] = "tls"
|
stream_settings["security"] = "tls"
|
||||||
tls_settings = {
|
tls_settings = {
|
||||||
"serverName": self.domain or "localhost",
|
"serverName": "localhost", # Will be replaced with server hostname when deployed
|
||||||
"alpn": ["h2", "http/1.1"]
|
"alpn": ["h2", "http/1.1"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,8 +342,8 @@ class Inbound(models.Model):
|
|||||||
stream_settings["security"] = "reality"
|
stream_settings["security"] = "reality"
|
||||||
# Reality settings would be configured here
|
# Reality settings would be configured here
|
||||||
stream_settings["realitySettings"] = {
|
stream_settings["realitySettings"] = {
|
||||||
"dest": self.domain or "example.com:443",
|
"dest": "example.com:443", # Will be replaced with server hostname when deployed
|
||||||
"serverNames": [self.domain] if self.domain else ["example.com"],
|
"serverNames": ["example.com"], # Will be replaced with server hostname when deployed
|
||||||
"privateKey": "", # Would be generated
|
"privateKey": "", # Would be generated
|
||||||
"shortIds": [""] # Would be generated
|
"shortIds": [""] # Would be generated
|
||||||
}
|
}
|
||||||
@@ -380,8 +375,8 @@ class SubscriptionGroup(models.Model):
|
|||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Subscription Group"
|
verbose_name = "Subscriptions"
|
||||||
verbose_name_plural = "Subscription Groups"
|
verbose_name_plural = "Subscriptions"
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@@ -535,7 +535,7 @@ class XrayServerV2(Server):
|
|||||||
try:
|
try:
|
||||||
# Generate connection string directly
|
# Generate connection string directly
|
||||||
from vpn.views import generate_xray_connection_string
|
from vpn.views import generate_xray_connection_string
|
||||||
connection_string = generate_xray_connection_string(user, inbound)
|
connection_string = generate_xray_connection_string(user, inbound, self.name, self.client_hostname)
|
||||||
|
|
||||||
if connection_string:
|
if connection_string:
|
||||||
configs.append({
|
configs.append({
|
||||||
@@ -680,11 +680,22 @@ class XrayServerV2(Server):
|
|||||||
return f"Xray Server v2: {self.name}"
|
return f"Xray Server v2: {self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class ServerInboundInline(admin.TabularInline):
|
||||||
|
"""Inline for managing inbound templates on a server"""
|
||||||
|
from vpn.models_xray import ServerInbound
|
||||||
|
model = ServerInbound
|
||||||
|
extra = 0
|
||||||
|
fields = ('inbound', 'active')
|
||||||
|
verbose_name = "Inbound Template"
|
||||||
|
verbose_name_plural = "Inbound Templates"
|
||||||
|
|
||||||
|
|
||||||
class XrayServerV2Admin(admin.ModelAdmin):
|
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']
|
||||||
|
inlines = [ServerInboundInline]
|
||||||
|
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
('Basic Information', {
|
('Basic Information', {
|
||||||
@@ -693,7 +704,7 @@ class XrayServerV2Admin(admin.ModelAdmin):
|
|||||||
('Connection Settings', {
|
('Connection Settings', {
|
||||||
'fields': ('client_hostname', 'api_address')
|
'fields': ('client_hostname', 'api_address')
|
||||||
}),
|
}),
|
||||||
('Features', {
|
('API Settings', {
|
||||||
'fields': ('api_enabled', 'stats_enabled')
|
'fields': ('api_enabled', 'stats_enabled')
|
||||||
}),
|
}),
|
||||||
('Timestamps', {
|
('Timestamps', {
|
||||||
|
@@ -175,6 +175,43 @@ def subscription_group_inbounds_changed(sender, instance, action, pk_set, **kwar
|
|||||||
transaction.on_commit(lambda: job.apply_async())
|
transaction.on_commit(lambda: job.apply_async())
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=ServerInbound)
|
||||||
|
def server_inbound_created_or_updated(sender, instance, created, **kwargs):
|
||||||
|
"""
|
||||||
|
When ServerInbound is created or updated, immediately deploy the inbound
|
||||||
|
template to the server (not wait for subscription group changes)
|
||||||
|
"""
|
||||||
|
logger.info(f"ServerInbound {instance.inbound.name} {'created' if created else 'updated'} for server {instance.server.name}")
|
||||||
|
|
||||||
|
if instance.active:
|
||||||
|
# Deploy inbound immediately
|
||||||
|
servers = [instance.server.get_real_instance()]
|
||||||
|
transaction.on_commit(
|
||||||
|
lambda: schedule_inbound_sync_for_servers(instance.inbound, servers)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Schedule user sync after inbound deployment
|
||||||
|
transaction.on_commit(lambda: schedule_user_sync_for_servers(servers))
|
||||||
|
else:
|
||||||
|
# Remove inbound from server if deactivated
|
||||||
|
logger.info(f"Removing inbound {instance.inbound.name} from server {instance.server.name} (deactivated)")
|
||||||
|
from .tasks import remove_inbound_from_server
|
||||||
|
task = remove_inbound_from_server.s(instance.server.id, instance.inbound.name)
|
||||||
|
transaction.on_commit(lambda: task.apply_async())
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_delete, sender=ServerInbound)
|
||||||
|
def server_inbound_deleted(sender, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
When ServerInbound is deleted, remove the inbound from the server
|
||||||
|
"""
|
||||||
|
logger.info(f"ServerInbound {instance.inbound.name} deleted from server {instance.server.name}")
|
||||||
|
|
||||||
|
from .tasks import remove_inbound_from_server
|
||||||
|
task = remove_inbound_from_server.s(instance.server.id, instance.inbound.name)
|
||||||
|
transaction.on_commit(lambda: task.apply_async())
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=UserSubscription)
|
@receiver(post_save, sender=UserSubscription)
|
||||||
def user_subscription_created_or_updated(sender, instance, created, **kwargs):
|
def user_subscription_created_or_updated(sender, instance, created, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -269,4 +306,41 @@ def subscription_group_updated(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
if tasks:
|
if tasks:
|
||||||
job = group(tasks)
|
job = group(tasks)
|
||||||
transaction.on_commit(lambda: job.apply_async())
|
transaction.on_commit(lambda: job.apply_async())
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=ServerInbound)
|
||||||
|
def server_inbound_created_or_updated(sender, instance, created, **kwargs):
|
||||||
|
"""
|
||||||
|
When ServerInbound is created or updated, immediately deploy the inbound
|
||||||
|
template to the server (not wait for subscription group changes)
|
||||||
|
"""
|
||||||
|
logger.info(f"ServerInbound {instance.inbound.name} {'created' if created else 'updated'} for server {instance.server.name}")
|
||||||
|
|
||||||
|
if instance.active:
|
||||||
|
# Deploy inbound immediately
|
||||||
|
servers = [instance.server.get_real_instance()]
|
||||||
|
transaction.on_commit(
|
||||||
|
lambda: schedule_inbound_sync_for_servers(instance.inbound, servers)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Schedule user sync after inbound deployment
|
||||||
|
transaction.on_commit(lambda: schedule_user_sync_for_servers(servers))
|
||||||
|
else:
|
||||||
|
# Remove inbound from server if deactivated
|
||||||
|
logger.info(f"Removing inbound {instance.inbound.name} from server {instance.server.name} (deactivated)")
|
||||||
|
from .tasks import remove_inbound_from_server
|
||||||
|
task = remove_inbound_from_server.s(instance.server.id, instance.inbound.name)
|
||||||
|
transaction.on_commit(lambda: task.apply_async())
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_delete, sender=ServerInbound)
|
||||||
|
def server_inbound_deleted(sender, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
When ServerInbound is deleted, remove the inbound from the server
|
||||||
|
"""
|
||||||
|
logger.info(f"ServerInbound {instance.inbound.name} deleted from server {instance.server.name}")
|
||||||
|
|
||||||
|
from .tasks import remove_inbound_from_server
|
||||||
|
task = remove_inbound_from_server.s(instance.server.id, instance.inbound.name)
|
||||||
|
transaction.on_commit(lambda: task.apply_async())
|
20
vpn/templates/admin/vpn/subscriptiongroup/change_form.html
Normal file
20
vpn/templates/admin/vpn/subscriptiongroup/change_form.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends "admin/change_form.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if show_tab_navigation %}
|
||||||
|
<div class="module" style="margin-bottom: 20px;">
|
||||||
|
<div style="display: flex; border-bottom: 1px solid #ddd;">
|
||||||
|
<a href="{% url 'admin:vpn_subscriptiongroup_changelist' %}"
|
||||||
|
style="padding: 10px 20px; text-decoration: none; border-bottom: 3px solid #417690; color: #417690;">
|
||||||
|
📋 Subscription Groups
|
||||||
|
</a>
|
||||||
|
<a href="/admin/vpn/usersubscription/"
|
||||||
|
style="padding: 10px 20px; text-decoration: none; border-bottom: 3px solid transparent; color: #666;">
|
||||||
|
👥 User Subscriptions
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
20
vpn/templates/admin/vpn/subscriptiongroup/change_list.html
Normal file
20
vpn/templates/admin/vpn/subscriptiongroup/change_list.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends "admin/change_list.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if show_tab_navigation %}
|
||||||
|
<div class="module" style="margin-bottom: 20px;">
|
||||||
|
<div style="display: flex; border-bottom: 1px solid #ddd;">
|
||||||
|
<a href="{% url 'admin:vpn_subscriptiongroup_changelist' %}"
|
||||||
|
style="padding: 10px 20px; text-decoration: none; border-bottom: 3px solid {% if current_tab == 'subscription_groups' %}#417690{% else %}transparent{% endif %}; color: {% if current_tab == 'subscription_groups' %}#417690{% else %}#666{% endif %};">
|
||||||
|
📋 Subscription Groups
|
||||||
|
</a>
|
||||||
|
<a href="/admin/vpn/usersubscription/"
|
||||||
|
style="padding: 10px 20px; text-decoration: none; border-bottom: 3px solid {% if current_tab == 'user_subscriptions' %}#417690{% else %}transparent{% endif %}; color: {% if current_tab == 'user_subscriptions' %}#417690{% else %}#666{% endif %};">
|
||||||
|
👥 User Subscriptions
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
18
vpn/templates/admin/vpn/usersubscription/change_form.html
Normal file
18
vpn/templates/admin/vpn/usersubscription/change_form.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{% extends "admin/change_form.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="module" style="margin-bottom: 20px;">
|
||||||
|
<div style="display: flex; border-bottom: 1px solid #ddd;">
|
||||||
|
<a href="{% url 'admin:vpn_subscriptiongroup_changelist' %}"
|
||||||
|
style="padding: 10px 20px; text-decoration: none; border-bottom: 3px solid transparent; color: #666;">
|
||||||
|
📋 Subscription Groups
|
||||||
|
</a>
|
||||||
|
<a href="/admin/vpn/usersubscription/"
|
||||||
|
style="padding: 10px 20px; text-decoration: none; border-bottom: 3px solid #417690; color: #417690;">
|
||||||
|
👥 User Subscriptions
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
18
vpn/templates/admin/vpn/usersubscription/change_list.html
Normal file
18
vpn/templates/admin/vpn/usersubscription/change_list.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{% extends "admin/change_list.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="module" style="margin-bottom: 20px;">
|
||||||
|
<div style="display: flex; border-bottom: 1px solid #ddd;">
|
||||||
|
<a href="{% url 'admin:vpn_subscriptiongroup_changelist' %}"
|
||||||
|
style="padding: 10px 20px; text-decoration: none; border-bottom: 3px solid transparent; color: #666;">
|
||||||
|
📋 Subscription Groups
|
||||||
|
</a>
|
||||||
|
<a href="/admin/vpn/usersubscription/"
|
||||||
|
style="padding: 10px 20px; text-decoration: none; border-bottom: 3px solid #417690; color: #417690;">
|
||||||
|
👥 User Subscriptions
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
@@ -497,7 +497,7 @@
|
|||||||
<div class="server-info">
|
<div class="server-info">
|
||||||
<div class="server-name">{{ group_name }}</div>
|
<div class="server-name">{{ group_name }}</div>
|
||||||
<div class="server-stats">
|
<div class="server-stats">
|
||||||
<span class="connection-count">🔗 {{ group_data.inbounds|length }} inbound(s)</span>
|
<span class="connection-count">🔗 {{ group_data.deployed_count }} inbound(s)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="server-type">Xray Group</div>
|
<div class="server-type">Xray Group</div>
|
||||||
@@ -517,7 +517,7 @@
|
|||||||
<div class="link-info">
|
<div class="link-info">
|
||||||
<div class="link-comment">🚀 {{ group_name }} Subscription</div>
|
<div class="link-comment">🚀 {{ group_name }} Subscription</div>
|
||||||
<div class="link-stats">
|
<div class="link-stats">
|
||||||
<span class="last-used">🔗 {{ group_data.inbounds|length }} inbound(s)</span>
|
<span class="last-used">🔗 {{ group_data.deployed_count }} inbound(s)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="usage-chart">
|
<div class="usage-chart">
|
||||||
@@ -525,7 +525,7 @@
|
|||||||
<div style="display: flex; flex-direction: column; gap: 5px; align-items: center;">
|
<div style="display: flex; flex-direction: column; gap: 5px; align-items: center;">
|
||||||
{% for inbound_data in group_data.inbounds %}
|
{% for inbound_data in group_data.inbounds %}
|
||||||
<div style="background: rgba(74, 222, 128, 0.2); padding: 3px 8px; border-radius: 8px; font-size: 0.7rem; color: #4ade80;">
|
<div style="background: rgba(74, 222, 128, 0.2); padding: 3px 8px; border-radius: 8px; font-size: 0.7rem; color: #4ade80;">
|
||||||
{{ inbound_data.protocol|upper }}:{{ inbound_data.port }}
|
{{ inbound_data.server_name }}: {{ inbound_data.protocol|upper }}:{{ inbound_data.port }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
101
vpn/views.py
101
vpn/views.py
@@ -57,8 +57,12 @@ def userPortal(request, user_hash):
|
|||||||
group_name = group.name
|
group_name = group.name
|
||||||
logger.debug(f"Processing subscription group {group_name}")
|
logger.debug(f"Processing subscription group {group_name}")
|
||||||
|
|
||||||
# Get all inbounds for this group
|
# Get all deployed inbounds for this group (count actual server deployments)
|
||||||
group_inbounds = group.inbounds.all()
|
from .models_xray import ServerInbound
|
||||||
|
deployed_inbounds = ServerInbound.objects.filter(
|
||||||
|
inbound__in=group.inbounds.all(),
|
||||||
|
active=True
|
||||||
|
).select_related('inbound', 'server')
|
||||||
|
|
||||||
# Calculate connections for this specific group
|
# Calculate connections for this specific group
|
||||||
group_connections = AccessLog.objects.filter(
|
group_connections = AccessLog.objects.filter(
|
||||||
@@ -73,9 +77,12 @@ def userPortal(request, user_hash):
|
|||||||
'subscription': subscription,
|
'subscription': subscription,
|
||||||
'inbounds': [],
|
'inbounds': [],
|
||||||
'total_connections': group_connections,
|
'total_connections': group_connections,
|
||||||
|
'deployed_count': deployed_inbounds.count(), # Actual deployed inbounds count
|
||||||
}
|
}
|
||||||
|
|
||||||
for inbound in group_inbounds:
|
# Process each deployed inbound (each server-inbound combination)
|
||||||
|
for server_inbound in deployed_inbounds:
|
||||||
|
inbound = server_inbound.inbound
|
||||||
logger.debug(f"Processing inbound {inbound.name} in group {group_name}")
|
logger.debug(f"Processing inbound {inbound.name} in group {group_name}")
|
||||||
|
|
||||||
# Generate connection URLs based on protocol
|
# Generate connection URLs based on protocol
|
||||||
@@ -84,7 +91,7 @@ def userPortal(request, user_hash):
|
|||||||
if inbound.protocol == 'vless':
|
if inbound.protocol == 'vless':
|
||||||
# Generate VLESS URL - this is a placeholder implementation
|
# Generate VLESS URL - this is a placeholder implementation
|
||||||
# In the real implementation, you'd generate proper VLESS URLs with user UUID
|
# In the real implementation, you'd generate proper VLESS URLs with user UUID
|
||||||
connection_url = f"vless://user-uuid@{inbound.domain or EXTERNAL_ADDRESS}:{inbound.port}#{inbound.name}"
|
connection_url = f"vless://user-uuid@{EXTERNAL_ADDRESS}:{inbound.port}#{inbound.name}"
|
||||||
connection_urls.append({
|
connection_urls.append({
|
||||||
'url': connection_url,
|
'url': connection_url,
|
||||||
'protocol': 'VLESS',
|
'protocol': 'VLESS',
|
||||||
@@ -92,7 +99,7 @@ def userPortal(request, user_hash):
|
|||||||
})
|
})
|
||||||
elif inbound.protocol == 'vmess':
|
elif inbound.protocol == 'vmess':
|
||||||
# Generate VMess URL - placeholder
|
# Generate VMess URL - placeholder
|
||||||
connection_url = f"vmess://user-config@{inbound.domain or EXTERNAL_ADDRESS}:{inbound.port}#{inbound.name}"
|
connection_url = f"vmess://user-config@{EXTERNAL_ADDRESS}:{inbound.port}#{inbound.name}"
|
||||||
connection_urls.append({
|
connection_urls.append({
|
||||||
'url': connection_url,
|
'url': connection_url,
|
||||||
'protocol': 'VMess',
|
'protocol': 'VMess',
|
||||||
@@ -100,7 +107,7 @@ def userPortal(request, user_hash):
|
|||||||
})
|
})
|
||||||
elif inbound.protocol == 'trojan':
|
elif inbound.protocol == 'trojan':
|
||||||
# Generate Trojan URL - placeholder
|
# Generate Trojan URL - placeholder
|
||||||
connection_url = f"trojan://user-password@{inbound.domain or EXTERNAL_ADDRESS}:{inbound.port}#{inbound.name}"
|
connection_url = f"trojan://user-password@{EXTERNAL_ADDRESS}:{inbound.port}#{inbound.name}"
|
||||||
connection_urls.append({
|
connection_urls.append({
|
||||||
'url': connection_url,
|
'url': connection_url,
|
||||||
'protocol': 'Trojan',
|
'protocol': 'Trojan',
|
||||||
@@ -108,7 +115,7 @@ def userPortal(request, user_hash):
|
|||||||
})
|
})
|
||||||
elif inbound.protocol == 'shadowsocks':
|
elif inbound.protocol == 'shadowsocks':
|
||||||
# Generate Shadowsocks URL - placeholder
|
# Generate Shadowsocks URL - placeholder
|
||||||
connection_url = f"ss://user-config@{inbound.domain or EXTERNAL_ADDRESS}:{inbound.port}#{inbound.name}"
|
connection_url = f"ss://user-config@{EXTERNAL_ADDRESS}:{inbound.port}#{inbound.name}"
|
||||||
connection_urls.append({
|
connection_urls.append({
|
||||||
'url': connection_url,
|
'url': connection_url,
|
||||||
'protocol': 'Shadowsocks',
|
'protocol': 'Shadowsocks',
|
||||||
@@ -120,11 +127,13 @@ def userPortal(request, user_hash):
|
|||||||
'connection_urls': connection_urls,
|
'connection_urls': connection_urls,
|
||||||
'protocol': inbound.protocol.upper(),
|
'protocol': inbound.protocol.upper(),
|
||||||
'port': inbound.port,
|
'port': inbound.port,
|
||||||
'domain': inbound.domain or EXTERNAL_ADDRESS,
|
'domain': EXTERNAL_ADDRESS,
|
||||||
'network': inbound.network,
|
'network': inbound.network,
|
||||||
'security': inbound.security,
|
'security': inbound.security,
|
||||||
'connections': 0, # Placeholder during transition
|
'connections': 0, # Placeholder during transition
|
||||||
'last_access_display': "Never used", # Placeholder
|
'last_access_display': "Never used", # Placeholder
|
||||||
|
'server': server_inbound.server, # Server that deployed this inbound
|
||||||
|
'server_name': server_inbound.server.name, # Server name for display
|
||||||
}
|
}
|
||||||
|
|
||||||
groups_data[group_name]['inbounds'].append(inbound_data)
|
groups_data[group_name]['inbounds'].append(inbound_data)
|
||||||
@@ -365,12 +374,23 @@ def xray_subscription(request, user_hash):
|
|||||||
# Get all inbounds from this group
|
# Get all inbounds from this group
|
||||||
for inbound in group.inbounds.all():
|
for inbound in group.inbounds.all():
|
||||||
try:
|
try:
|
||||||
# Generate connection string based on protocol
|
# Find all servers where this inbound is deployed
|
||||||
connection_string = generate_xray_connection_string(user, inbound)
|
from .models_xray import ServerInbound
|
||||||
|
deployed_servers = ServerInbound.objects.filter(
|
||||||
|
inbound=inbound,
|
||||||
|
active=True
|
||||||
|
).select_related('server')
|
||||||
|
|
||||||
if connection_string:
|
# Generate connection string for each server where inbound is deployed
|
||||||
subscription_configs.append(connection_string)
|
for server_inbound in deployed_servers:
|
||||||
logger.info(f"Added {inbound.protocol} config for inbound {inbound.name}")
|
server = server_inbound.server
|
||||||
|
# Get server's client_hostname for XrayServerV2
|
||||||
|
server_hostname = getattr(server.get_real_instance(), 'client_hostname', None)
|
||||||
|
connection_string = generate_xray_connection_string(user, inbound, server.name, server_hostname)
|
||||||
|
|
||||||
|
if connection_string:
|
||||||
|
subscription_configs.append(connection_string)
|
||||||
|
logger.info(f"Added {inbound.protocol} config for inbound {inbound.name} on server {server.name}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to generate config for inbound {inbound.name}: {e}")
|
logger.warning(f"Failed to generate config for inbound {inbound.name}: {e}")
|
||||||
@@ -449,17 +469,29 @@ def xray_subscription_json(request, user, user_hash):
|
|||||||
# Get all inbounds from this group
|
# Get all inbounds from this group
|
||||||
for inbound in group.inbounds.all():
|
for inbound in group.inbounds.all():
|
||||||
try:
|
try:
|
||||||
# Generate connection string
|
# Find all servers where this inbound is deployed
|
||||||
connection_string = generate_xray_connection_string(user, inbound)
|
from .models_xray import ServerInbound
|
||||||
|
deployed_servers = ServerInbound.objects.filter(
|
||||||
|
inbound=inbound,
|
||||||
|
active=True
|
||||||
|
).select_related('server')
|
||||||
|
|
||||||
if connection_string:
|
# Generate connection string for each server where inbound is deployed
|
||||||
group_configs.append({
|
for server_inbound in deployed_servers:
|
||||||
'name': inbound.name,
|
server = server_inbound.server
|
||||||
|
# Get server's client_hostname for XrayServerV2
|
||||||
|
server_hostname = getattr(server.get_real_instance(), 'client_hostname', None)
|
||||||
|
connection_string = generate_xray_connection_string(user, inbound, server.name, server_hostname)
|
||||||
|
|
||||||
|
if connection_string:
|
||||||
|
config_name = f"{server.name} {inbound.name}"
|
||||||
|
group_configs.append({
|
||||||
|
'name': config_name,
|
||||||
'protocol': inbound.protocol.upper(),
|
'protocol': inbound.protocol.upper(),
|
||||||
'port': inbound.port,
|
'port': inbound.port,
|
||||||
'network': inbound.network,
|
'network': inbound.network,
|
||||||
'security': inbound.security,
|
'security': inbound.security,
|
||||||
'domain': inbound.domain,
|
'domain': host,
|
||||||
'connection_string': connection_string
|
'connection_string': connection_string
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -481,7 +513,7 @@ def xray_subscription_json(request, user, user_hash):
|
|||||||
return JsonResponse({'error': str(e)}, status=500)
|
return JsonResponse({'error': str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
def generate_xray_connection_string(user, inbound):
|
def generate_xray_connection_string(user, inbound, server_name=None, server_hostname=None):
|
||||||
"""Generate Xray connection string for user and inbound"""
|
"""Generate Xray connection string for user and inbound"""
|
||||||
import uuid
|
import uuid
|
||||||
import base64
|
import base64
|
||||||
@@ -492,8 +524,8 @@ def generate_xray_connection_string(user, inbound):
|
|||||||
# Generate user UUID based on user ID and inbound
|
# Generate user UUID based on user ID and inbound
|
||||||
user_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"{user.username}-{inbound.name}"))
|
user_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"{user.username}-{inbound.name}"))
|
||||||
|
|
||||||
# Get host (domain or EXTERNAL_ADDRESS)
|
# Get host (use server's client_hostname if available, fallback to EXTERNAL_ADDRESS)
|
||||||
host = inbound.domain if inbound.domain else EXTERNAL_ADDRESS
|
host = server_hostname if server_hostname else EXTERNAL_ADDRESS
|
||||||
|
|
||||||
if inbound.protocol == 'vless':
|
if inbound.protocol == 'vless':
|
||||||
# VLESS URL format: vless://uuid@host:port?params#name
|
# VLESS URL format: vless://uuid@host:port?params#name
|
||||||
@@ -504,8 +536,8 @@ def generate_xray_connection_string(user, inbound):
|
|||||||
|
|
||||||
if inbound.security != 'none':
|
if inbound.security != 'none':
|
||||||
params.append(f"security={inbound.security}")
|
params.append(f"security={inbound.security}")
|
||||||
if inbound.security == 'tls' and inbound.domain:
|
if inbound.security == 'tls' and host:
|
||||||
params.append(f"sni={inbound.domain}")
|
params.append(f"sni={host}")
|
||||||
|
|
||||||
if inbound.network == 'ws':
|
if inbound.network == 'ws':
|
||||||
params.append(f"path=/{inbound.name}")
|
params.append(f"path=/{inbound.name}")
|
||||||
@@ -515,13 +547,17 @@ def generate_xray_connection_string(user, inbound):
|
|||||||
param_string = '&'.join(params)
|
param_string = '&'.join(params)
|
||||||
query_part = f"?{param_string}" if param_string else ""
|
query_part = f"?{param_string}" if param_string else ""
|
||||||
|
|
||||||
connection_string = f"vless://{user_uuid}@{host}:{inbound.port}{query_part}#{quote(inbound.name)}"
|
# Generate config name: ServerName InboundName (e.g., "Israel VLESS-Premium")
|
||||||
|
config_name = f"{server_name} {inbound.name}" if server_name else inbound.name
|
||||||
|
connection_string = f"vless://{user_uuid}@{host}:{inbound.port}{query_part}#{quote(config_name)}"
|
||||||
|
|
||||||
elif inbound.protocol == 'vmess':
|
elif inbound.protocol == 'vmess':
|
||||||
# VMess JSON format encoded in base64
|
# VMess JSON format encoded in base64
|
||||||
|
# Generate config name: ServerName InboundName (e.g., "Israel VMESS-Premium")
|
||||||
|
config_name = f"{server_name} {inbound.name}" if server_name else inbound.name
|
||||||
vmess_config = {
|
vmess_config = {
|
||||||
"v": "2",
|
"v": "2",
|
||||||
"ps": inbound.name,
|
"ps": config_name,
|
||||||
"add": host,
|
"add": host,
|
||||||
"port": str(inbound.port),
|
"port": str(inbound.port),
|
||||||
"id": user_uuid,
|
"id": user_uuid,
|
||||||
@@ -529,7 +565,7 @@ def generate_xray_connection_string(user, inbound):
|
|||||||
"scy": "auto",
|
"scy": "auto",
|
||||||
"net": inbound.network,
|
"net": inbound.network,
|
||||||
"type": "none",
|
"type": "none",
|
||||||
"host": inbound.domain if inbound.domain else "",
|
"host": host if host else "",
|
||||||
"path": f"/{inbound.name}" if inbound.network == 'ws' else "",
|
"path": f"/{inbound.name}" if inbound.network == 'ws' else "",
|
||||||
"tls": inbound.security if inbound.security != 'none' else ""
|
"tls": inbound.security if inbound.security != 'none' else ""
|
||||||
}
|
}
|
||||||
@@ -543,8 +579,8 @@ def generate_xray_connection_string(user, inbound):
|
|||||||
# Use user UUID as password
|
# Use user UUID as password
|
||||||
params = []
|
params = []
|
||||||
|
|
||||||
if inbound.security != 'none' and inbound.domain:
|
if inbound.security != 'none' and host:
|
||||||
params.append(f"sni={inbound.domain}")
|
params.append(f"sni={host}")
|
||||||
|
|
||||||
if inbound.network != 'tcp':
|
if inbound.network != 'tcp':
|
||||||
params.append(f"type={inbound.network}")
|
params.append(f"type={inbound.network}")
|
||||||
@@ -556,11 +592,14 @@ def generate_xray_connection_string(user, inbound):
|
|||||||
param_string = '&'.join(params)
|
param_string = '&'.join(params)
|
||||||
query_part = f"?{param_string}" if param_string else ""
|
query_part = f"?{param_string}" if param_string else ""
|
||||||
|
|
||||||
connection_string = f"trojan://{user_uuid}@{host}:{inbound.port}{query_part}#{quote(inbound.name)}"
|
# Generate config name: ServerName InboundName (e.g., "Israel TROJAN-Premium")
|
||||||
|
config_name = f"{server_name} {inbound.name}" if server_name else inbound.name
|
||||||
|
connection_string = f"trojan://{user_uuid}@{host}:{inbound.port}{query_part}#{quote(config_name)}"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Fallback for unknown protocols
|
# Fallback for unknown protocols
|
||||||
connection_string = f"{inbound.protocol}://{user_uuid}@{host}:{inbound.port}#{quote(inbound.name)}"
|
config_name = f"{server_name} {inbound.name}" if server_name else inbound.name
|
||||||
|
connection_string = f"{inbound.protocol}://{user_uuid}@{host}:{inbound.port}#{quote(config_name)}"
|
||||||
|
|
||||||
return connection_string
|
return connection_string
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user