""" Admin interface for new Xray models. """ import json from django.contrib import admin, messages from django.utils.safestring import mark_safe from django.utils.html import format_html from django.db import models from django.forms import CheckboxSelectMultiple, Textarea from django.shortcuts import render, redirect from django.urls import path, reverse from django.http import JsonResponse, HttpResponseRedirect from .models_xray import ( XrayConfiguration, Credentials, Certificate, Inbound, SubscriptionGroup, UserSubscription, ServerInbound ) @admin.register(XrayConfiguration) class XrayConfigurationAdmin(admin.ModelAdmin): """Admin for global Xray configuration""" list_display = ('grpc_address', 'default_client_hostname', 'stats_enabled', 'cert_renewal_days', 'updated_at') fields = ( 'grpc_address', 'default_client_hostname', 'stats_enabled', 'cert_renewal_days', 'created_at', 'updated_at' ) readonly_fields = ('created_at', 'updated_at') def has_add_permission(self, request): # Only allow one configuration return not XrayConfiguration.objects.exists() def has_delete_permission(self, request, obj=None): return False @admin.register(Credentials) class CredentialsAdmin(admin.ModelAdmin): """Admin for credentials management""" list_display = ('name', 'cred_type', 'description', 'created_at') list_filter = ('cred_type',) search_fields = ('name', 'description') fieldsets = ( ('Basic Information', { 'fields': ('name', 'cred_type', 'description') }), ('Credentials Data', { 'fields': ('credentials_help', 'credentials'), 'description': 'Enter credentials as JSON. Example: {"api_token": "your_token", "email": "your_email"}' }), ('Preview', { 'fields': ('credentials_display',), 'classes': ('collapse',), }), ('Timestamps', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }) ) readonly_fields = ('credentials_display', 'credentials_help', 'created_at', 'updated_at') # Add JSON widget for better formatting formfield_overrides = { models.JSONField: {'widget': Textarea(attrs={'rows': 10, 'cols': 80, 'class': 'vLargeTextField'})}, } def credentials_help(self, obj): """Help text and examples for credentials field""" examples = { 'cloudflare': { 'api_token': 'your_cloudflare_api_token', 'email': 'your_email@example.com' }, 'dns_provider': { 'api_key': 'your_dns_api_key', 'secret': 'your_secret' }, 'email': { 'smtp_host': 'smtp.example.com', 'smtp_port': 587, 'username': 'your_email', 'password': 'your_password' } } html = '
' html += '

JSON Examples:

' for cred_type, example in examples.items(): html += '
' html += '' + cred_type.title() + ':' json_str = json.dumps(example, indent=2) html += '
' + json_str + '
' html += '
' html += '

Note: Make sure your JSON is valid. Use double quotes for strings.

' html += '
' return mark_safe(html) credentials_help.short_description = 'Credentials Format Help' def credentials_display(self, obj): """Display credentials in a safe format""" if obj.credentials: # Hide sensitive values safe_creds = {} for key, value in obj.credentials.items(): if any(sensitive in key.lower() for sensitive in ['token', 'key', 'password', 'secret']): safe_creds[key] = '*' * 8 else: safe_creds[key] = value return format_html( '
{}
', json.dumps(safe_creds, indent=2) ) return '-' credentials_display.short_description = 'Credentials (Preview)' @admin.register(Certificate) class CertificateAdmin(admin.ModelAdmin): """Admin for certificate management""" list_display = ( 'domain', 'cert_type', 'status_display', 'expires_at', 'auto_renew', 'action_buttons' ) list_filter = ('cert_type', 'auto_renew') search_fields = ('domain',) fieldsets = ( ('Certificate Request', { 'fields': ('domain', 'cert_type', 'acme_email', 'credentials', 'auto_renew'), 'description': 'For Let\'s Encrypt certificates, provide email for ACME registration and select credentials with Cloudflare API token.' }), ('Certificate Status', { 'fields': ('generation_help', 'status_display', 'expires_at'), 'classes': ('wide',) }), ('Certificate Data', { 'fields': ('certificate_preview', 'certificate_pem', 'private_key_pem'), 'classes': ('collapse',), 'description': 'Certificate data (auto-generated for Let\'s Encrypt)' }), ('Renewal Settings', { 'fields': ('last_renewed',), 'classes': ('collapse',) }), ('Timestamps', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }) ) readonly_fields = ( 'certificate_preview', 'status_display', 'generation_help', 'expires_at', 'last_renewed', 'created_at', 'updated_at' ) def generation_help(self, obj): """Show help text for certificate generation""" if not obj.pk: return mark_safe('
' '

How it works:

' '
    ' '
  1. Fill in the domain name
  2. ' '
  3. Select certificate type (Let\'s Encrypt recommended)
  4. ' '
  5. For Let\'s Encrypt: provide email for ACME account registration
  6. ' '
  7. Select credentials with Cloudflare API token
  8. ' '
  9. Save - certificate will be generated automatically
  10. ' '
' '
') if obj.cert_type == 'letsencrypt' and not obj.certificate_pem: return mark_safe('
' '

⏳ Certificate not generated yet

' '

Certificate will be generated automatically using Let\'s Encrypt DNS-01 challenge.

' '
') if obj.certificate_pem: days = obj.days_until_expiration if obj.days_until_expiration is not None else 'Unknown' return mark_safe('
' '

✅ Certificate generated successfully

' f'

Expires: {obj.expires_at}

' f'

Days remaining: {days}

' '
') return '-' generation_help.short_description = 'Certificate Generation Status' def status_display(self, obj): """Display certificate status""" if obj.is_expired: return format_html( '❌ Expired' ) elif obj.needs_renewal: return format_html( '⚠️ Needs renewal ({} days)', obj.days_until_expiration ) else: return format_html( '✅ Valid ({} days)', obj.days_until_expiration ) status_display.short_description = 'Status' def certificate_preview(self, obj): """Preview certificate info""" if obj.certificate_pem: lines = obj.certificate_pem.strip().split('\n') preview = '\n'.join(lines[:5] + ['...'] + lines[-3:]) return format_html( '
{}
', preview ) return '-' certificate_preview.short_description = 'Certificate Preview' def action_buttons(self, obj): """Action buttons for certificate""" buttons = [] if obj.needs_renewal and obj.auto_renew: renew_url = reverse('admin:certificate_renew', args=[obj.pk]) buttons.append( f'🔄 Renew Now' ) if obj.cert_type == 'self_signed': regenerate_url = reverse('admin:certificate_regenerate', args=[obj.pk]) buttons.append( f'🔄 Regenerate' ) return format_html(' '.join(buttons)) if buttons else '-' action_buttons.short_description = 'Actions' def get_urls(self): urls = super().get_urls() custom_urls = [ path('/renew/', self.admin_site.admin_view(self.renew_certificate_view), name='certificate_renew'), path('/regenerate/', self.admin_site.admin_view(self.regenerate_certificate_view), name='certificate_regenerate'), ] return custom_urls + urls def renew_certificate_view(self, request, cert_id): """Renew Let's Encrypt certificate""" try: cert = Certificate.objects.get(pk=cert_id) # TODO: Implement renewal logic messages.success(request, f'Certificate for {cert.domain} renewed successfully!') except Exception as e: messages.error(request, f'Failed to renew certificate: {e}') return redirect('admin:vpn_certificate_change', cert_id) def regenerate_certificate_view(self, request, cert_id): """Regenerate self-signed certificate""" try: cert = Certificate.objects.get(pk=cert_id) # TODO: Implement regeneration logic messages.success(request, f'Certificate for {cert.domain} regenerated successfully!') except Exception as e: messages.error(request, f'Failed to regenerate certificate: {e}') return redirect('admin:vpn_certificate_change', cert_id) def save_model(self, request, obj, form, change): """Auto-generate certificate for Let's Encrypt after saving""" super().save_model(request, obj, form, change) # Auto-generate Let's Encrypt certificate if needed if obj.cert_type == 'letsencrypt' and not obj.certificate_pem: try: self.generate_letsencrypt_certificate(obj, request) except Exception as e: messages.warning(request, f'Certificate saved but auto-generation failed: {e}') def generate_letsencrypt_certificate(self, cert_obj, request): """Generate Let's Encrypt certificate using DNS-01 challenge""" if not cert_obj.credentials: messages.error(request, 'Credentials required for Let\'s Encrypt certificate generation') return if not cert_obj.acme_email: messages.error(request, 'ACME email address required for Let\'s Encrypt certificate generation') return try: from vpn.letsencrypt.letsencrypt_dns import get_certificate_for_domain from datetime import datetime, timedelta from django.utils import timezone # Get Cloudflare credentials api_token = cert_obj.credentials.get_credential('api_token') if not api_token: messages.error(request, 'Cloudflare API token not found in credentials') return messages.info(request, f'🔄 Generating Let\'s Encrypt certificate for {cert_obj.domain} using {cert_obj.acme_email}...') # Schedule certificate generation via Celery from vpn.tasks import generate_certificate_task task = generate_certificate_task.delay(cert_obj.id) messages.success( request, f'🔄 Certificate generation scheduled for {cert_obj.domain}. Task ID: {task.id}' ) except ImportError: messages.warning(request, 'Let\'s Encrypt DNS challenge library not available') except Exception as e: messages.error(request, f'Failed to generate certificate: {str(e)}') # Log the full error for debugging import logging logger = logging.getLogger(__name__) logger.error(f'Certificate generation failed for {cert_obj.domain}: {e}', exc_info=True) @admin.register(Inbound) class InboundAdmin(admin.ModelAdmin): """Admin for inbound management""" list_display = ( 'name', 'protocol', 'port', 'network', 'security', 'certificate_status', 'group_count' ) list_filter = ('protocol', 'network', 'security') search_fields = ('name', 'domain') fieldsets = ( ('Basic Configuration', { 'fields': ('name', 'protocol', 'port', 'domain') }), ('Transport & Security', { 'fields': ('network', 'security', 'certificate', 'listen_address') }), ('Advanced Settings', { 'fields': ('enable_sniffing', 'full_config_display'), 'classes': ('collapse',), 'description': 'Configuration is auto-generated based on basic settings above' }), ('Timestamps', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }) ) readonly_fields = ('full_config_display', 'created_at', 'updated_at') def certificate_status(self, obj): """Display certificate status""" if obj.security == 'tls': if obj.certificate: if obj.certificate.is_expired: return format_html('❌ Expired') else: return format_html('✅ Valid') else: return format_html('⚠️ No cert') return format_html('-') certificate_status.short_description = 'Cert Status' def group_count(self, obj): """Number of groups this inbound belongs to""" return obj.subscriptiongroup_set.count() group_count.short_description = 'Groups' def full_config_display(self, obj): """Display full config in formatted JSON""" if obj.full_config: return format_html( '
{}
', json.dumps(obj.full_config, indent=2) ) return 'Not generated yet' full_config_display.short_description = 'Configuration Preview' def save_model(self, request, obj, form, change): """Generate config on save""" try: # Always regenerate config to reflect any changes obj.build_config() messages.success(request, f'✅ Configuration generated successfully for {obj.protocol.upper()} inbound on port {obj.port}') except Exception as e: messages.warning(request, f'Inbound saved but config generation failed: {e}') # Set empty dict if generation fails if not obj.full_config: obj.full_config = {} super().save_model(request, obj, form, change) class InboundInline(admin.TabularInline): """Inline for inbounds in subscription groups""" model = SubscriptionGroup.inbounds.through extra = 1 verbose_name = "Inbound" verbose_name_plural = "Inbounds in this group" @admin.register(SubscriptionGroup) class SubscriptionGroupAdmin(admin.ModelAdmin): """Admin for subscription groups""" list_display = ('name', 'is_active', 'inbound_count', 'user_count', 'created_at') list_filter = ('is_active',) search_fields = ('name', 'description') filter_horizontal = ('inbounds',) fieldsets = ( ('Group Information', { 'fields': ('name', 'description', 'is_active') }), ('Inbounds', { 'fields': ('inbounds',), 'description': 'Select inbounds to include in this group' }), ('Statistics', { 'fields': ('group_statistics',), 'classes': ('collapse',) }) ) readonly_fields = ('group_statistics',) 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 = '
' for key, value in stats.items(): if isinstance(value, list): value = ', '.join(map(str, value)) html += f'
{key}: {value}
' html += '
' return format_html(html) return 'Save to see statistics' group_statistics.short_description = 'Group Statistics' class UserSubscriptionInline(admin.TabularInline): """Inline for user subscriptions""" model = UserSubscription extra = 0 fields = ('subscription_group', 'active', 'created_at') readonly_fields = ('created_at',) verbose_name = "Subscription Group" verbose_name_plural = "User's Subscription Groups" # Extension for User admin def add_subscription_management_to_user(UserAdmin): """Add subscription management to existing User admin""" # Add inline 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('
Save user first to manage subscriptions
') # 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 = '
' html += '

Available Subscription Groups:

' if all_groups: html += '
' for group in all_groups: checked = 'checked' if group.id in user_groups else '' status = '✅' if group.id in user_groups else '⬜' html += f'''
{status}
''' html += '
' html += '
' html += 'ℹ️ Use the inline form below to manage subscriptions' html += '
' else: html += '
No active subscription groups available
' html += '
' return mark_safe(html) subscription_groups_widget.short_description = 'Subscription Groups Overview' UserAdmin.subscription_groups_widget = subscription_groups_widget # Register admin for UserSubscription (if needed separately) @admin.register(UserSubscription) class UserSubscriptionAdmin(admin.ModelAdmin): """Standalone admin for user subscriptions""" list_display = ('user', 'subscription_group', 'active', 'created_at') list_filter = ('active', 'subscription_group') search_fields = ('user__username', 'subscription_group__name') date_hierarchy = 'created_at' def has_add_permission(self, request): # Prefer managing through User admin return False @admin.register(ServerInbound) class ServerInboundAdmin(admin.ModelAdmin): """Admin for server-inbound deployment tracking""" list_display = ('server', 'inbound', 'active', 'deployed_at', 'updated_at') list_filter = ('active', 'inbound__protocol', 'deployed_at') search_fields = ('server__name', 'inbound__name') date_hierarchy = 'deployed_at' fieldsets = ( ('Deployment', { 'fields': ('server', 'inbound', 'active') }), ('Configuration', { 'fields': ('deployment_config_display', 'deployment_config'), 'classes': ('collapse',) }), ('Timestamps', { 'fields': ('deployed_at', 'updated_at'), 'classes': ('collapse',) }) ) readonly_fields = ('deployment_config_display', 'deployed_at', 'updated_at') def deployment_config_display(self, obj): """Display deployment config in formatted JSON""" if obj.deployment_config: return format_html( '
{}
', json.dumps(obj.deployment_config, indent=2) ) return 'No additional deployment configuration' deployment_config_display.short_description = 'Deployment Config Preview'