diff --git a/vpn/admin.py b/vpn/admin.py index 554d1b6..6c43f2d 100644 --- a/vpn/admin.py +++ b/vpn/admin.py @@ -195,20 +195,20 @@ class LastAccessFilter(admin.SimpleListFilter): from datetime import timedelta if self.value() == 'never': - # Links that have no access logs - return queryset.filter(last_access__isnull=True) + # Links that have never been accessed + return queryset.filter(last_access_time__isnull=True) elif self.value() == 'week': # Links accessed in the last week week_ago = timezone.now() - timedelta(days=7) - return queryset.filter(last_access__gte=week_ago) + return queryset.filter(last_access_time__gte=week_ago) elif self.value() == 'month': # Links accessed in the last month month_ago = timezone.now() - timedelta(days=30) - return queryset.filter(last_access__gte=month_ago) + return queryset.filter(last_access_time__gte=month_ago) elif self.value() == 'old': # Links not accessed for more than 3 months three_months_ago = timezone.now() - timedelta(days=90) - return queryset.filter(last_access__lt=three_months_ago) + return queryset.filter(last_access_time__lt=three_months_ago) return queryset @admin.register(Server) @@ -683,17 +683,6 @@ class ACLLinkAdmin(admin.ModelAdmin): def get_queryset(self, request): qs = super().get_queryset(request) qs = qs.select_related('acl__user', 'acl__server') - - # Add last access annotation - qs = qs.annotate( - last_access=Subquery( - AccessLog.objects.filter( - user=OuterRef('acl__user__username'), - server=OuterRef('acl__server__name') - ).order_by('-timestamp').values('timestamp')[:1] - ) - ) - return qs @admin.display(description='Link', ordering='link') @@ -723,15 +712,15 @@ class ACLLinkAdmin(admin.ModelAdmin): return obj.comment[:50] + '...' if len(obj.comment) > 50 else obj.comment return '-' - @admin.display(description='Last Access', ordering='last_access') + @admin.display(description='Last Access', ordering='last_access_time') def last_access_display(self, obj): - if hasattr(obj, 'last_access') and obj.last_access: + if obj.last_access_time: from django.utils import timezone from datetime import timedelta - local_time = localtime(obj.last_access) + local_time = localtime(obj.last_access_time) now = timezone.now() - diff = now - obj.last_access + diff = now - obj.last_access_time # Color coding based on age if diff <= timedelta(days=7): @@ -794,17 +783,17 @@ class ACLLinkAdmin(admin.ModelAdmin): # Add summary statistics to the changelist extra_context = extra_context or {} - # Get queryset with annotations for statistics + # Get queryset for statistics queryset = self.get_queryset(request) total_links = queryset.count() - never_accessed = queryset.filter(last_access__isnull=True).count() + never_accessed = queryset.filter(last_access_time__isnull=True).count() from django.utils import timezone from datetime import timedelta three_months_ago = timezone.now() - timedelta(days=90) old_links = queryset.filter( - Q(last_access__lt=three_months_ago) | Q(last_access__isnull=True) + Q(last_access_time__lt=three_months_ago) | Q(last_access_time__isnull=True) ).count() extra_context.update({ @@ -817,7 +806,7 @@ class ACLLinkAdmin(admin.ModelAdmin): def get_ordering(self, request): """Allow sorting by annotated fields""" - # Handle sorting by last_access if requested + # Handle sorting by last_access_time if requested order_var = request.GET.get('o') if order_var: try: @@ -825,9 +814,9 @@ class ACLLinkAdmin(admin.ModelAdmin): # Check if this corresponds to the last_access column (index 4 in list_display) if field_index == 4: # last_access_display is at index 4 if order_var.startswith('-'): - return ['-last_access'] + return ['-last_access_time'] else: - return ['last_access'] + return ['last_access_time'] except (ValueError, IndexError): pass diff --git a/vpn/migrations/0003_acllink_last_access_time.py b/vpn/migrations/0003_acllink_last_access_time.py new file mode 100644 index 0000000..7f206d2 --- /dev/null +++ b/vpn/migrations/0003_acllink_last_access_time.py @@ -0,0 +1,18 @@ +# Generated migration for adding last_access_time field + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vpn', '0002_taskexecutionlog'), + ] + + operations = [ + migrations.AddField( + model_name='acllink', + name='last_access_time', + field=models.DateTimeField(blank=True, help_text='Last time this link was accessed', null=True), + ), + ] diff --git a/vpn/models.py b/vpn/models.py index c0fddd9..d67c71e 100644 --- a/vpn/models.py +++ b/vpn/models.py @@ -130,6 +130,7 @@ class ACLLink(models.Model): acl = models.ForeignKey(ACL, related_name='links', on_delete=models.CASCADE) comment = models.TextField(default="", blank=True, help_text="ACL link comment, device name, etc...") link = models.CharField(max_length=1024, default="", unique=True, blank=True, null=True, verbose_name="Access link", help_text="Access link to get dynamic configuration") + last_access_time = models.DateTimeField(null=True, blank=True, help_text="Last time this link was accessed") def save(self, *args, **kwargs): if self.link == "": diff --git a/vpn/views.py b/vpn/views.py index f697a78..775672d 100644 --- a/vpn/views.py +++ b/vpn/views.py @@ -1,8 +1,7 @@ def userPortal(request, user_hash): """HTML portal for user to view their VPN access links and server information""" - from .models import User, ACLLink, AccessLog + from .models import User, ACLLink import logging - from django.db.models import Count, Q logger = logging.getLogger(__name__) @@ -20,78 +19,10 @@ def userPortal(request, user_hash): # Get all ACL links for the user with server information acl_links = ACLLink.objects.filter(acl__user=user).select_related('acl__server', 'acl') - # Get connection statistics for all user's links - # Count successful connections for each specific link - connection_stats = {} - recent_connection_stats = {} - usage_frequency = {} - total_connections = 0 - - # Get date ranges for analysis - from django.utils import timezone - from datetime import timedelta - now = timezone.now() - recent_date = now - timedelta(days=30) - - for acl_link in acl_links: - # Since we can't reliably track individual link usage from AccessLog.data, - # we'll count all successful connections to the server for this user - # and distribute them among all links for this server - - # Get all successful connections for this user on this server - server_connections = AccessLog.objects.filter( - user=user.username, - server=acl_link.acl.server.name, - action='Success' - ).count() - - # Get recent connections (last 30 days) - server_recent_connections = AccessLog.objects.filter( - user=user.username, - server=acl_link.acl.server.name, - action='Success', - timestamp__gte=recent_date - ).count() - - # Get number of links for this server for this user - user_links_on_server = ACLLink.objects.filter( - acl__user=user, - acl__server=acl_link.acl.server - ).count() - - # Distribute connections evenly among links - link_connections = server_connections // user_links_on_server if user_links_on_server > 0 else 0 - recent_connections = server_recent_connections // user_links_on_server if user_links_on_server > 0 else 0 - - # Calculate daily usage for the last 30 days - daily_usage = [] - for i in range(30): - day_start = now - timedelta(days=i+1) - day_end = now - timedelta(days=i) - - day_connections = AccessLog.objects.filter( - user=user.username, - server=acl_link.acl.server.name, - action='Success', - timestamp__gte=day_start, - timestamp__lt=day_end - ).count() - - # Distribute daily connections among links - day_link_connections = day_connections // user_links_on_server if user_links_on_server > 0 else 0 - daily_usage.append(day_link_connections) - - # Reverse to show oldest to newest - daily_usage.reverse() - - connection_stats[acl_link.link] = link_connections - recent_connection_stats[acl_link.link] = recent_connections - usage_frequency[acl_link.link] = daily_usage - total_connections += link_connections - # Group links by server servers_data = {} total_links = 0 + total_connections = 0 # Can be calculated from AccessLog if needed for link in acl_links: server = link.acl.server @@ -119,34 +50,22 @@ def userPortal(request, user_hash): 'total_connections': 0, } - # Add link information with connection stats + # Add link information with simple last access from denormalized field link_url = f"{EXTERNAL_ADDRESS}/ss/{link.link}#{server_name}" - connection_count = connection_stats.get(link.link, 0) - recent_count = recent_connection_stats.get(link.link, 0) - daily_usage = usage_frequency.get(link.link, [0] * 30) servers_data[server_name]['links'].append({ 'link': link, 'url': link_url, 'comment': link.comment or 'Default', - 'connections': connection_count, - 'recent_connections': recent_count, - 'daily_usage': daily_usage, - 'max_daily': max(daily_usage) if daily_usage else 0, + 'last_access': link.last_access_time, }) - servers_data[server_name]['total_connections'] += connection_count total_links += 1 - # Calculate total recent connections from all links - total_recent_connections = sum(recent_connection_stats.values()) - context = { 'user': user, 'servers_data': servers_data, 'total_servers': len(servers_data), 'total_links': total_links, - 'total_connections': total_connections, - 'recent_connections': total_recent_connections, 'external_address': EXTERNAL_ADDRESS, } @@ -184,6 +103,7 @@ def userFrontend(request, user_hash): def shadowsocks(request, link): from .models import ACLLink, AccessLog import logging + from django.utils import timezone logger = logging.getLogger(__name__) @@ -244,6 +164,11 @@ def shadowsocks(request, link): } response = yaml.dump(config, allow_unicode=True) + # Update last access time for this specific link + acl_link.last_access_time = timezone.now() + acl_link.save(update_fields=['last_access_time']) + + # Still create AccessLog for audit purposes AccessLog.objects.create(user=acl.user, server=acl.server.name, action="Success", data=response) return HttpResponse(response, content_type=f"{ 'application/json; charset=utf-8' if request.GET.get('mode') == 'json' else 'text/html' }")