Improve acl model

This commit is contained in:
Ultradesu
2025-07-21 12:12:31 +03:00
parent 664bafe067
commit ba62e214ce
4 changed files with 44 additions and 111 deletions

View File

@@ -195,20 +195,20 @@ class LastAccessFilter(admin.SimpleListFilter):
from datetime import timedelta from datetime import timedelta
if self.value() == 'never': if self.value() == 'never':
# Links that have no access logs # Links that have never been accessed
return queryset.filter(last_access__isnull=True) return queryset.filter(last_access_time__isnull=True)
elif self.value() == 'week': elif self.value() == 'week':
# Links accessed in the last week # Links accessed in the last week
week_ago = timezone.now() - timedelta(days=7) 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': elif self.value() == 'month':
# Links accessed in the last month # Links accessed in the last month
month_ago = timezone.now() - timedelta(days=30) 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': elif self.value() == 'old':
# Links not accessed for more than 3 months # Links not accessed for more than 3 months
three_months_ago = timezone.now() - timedelta(days=90) 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 return queryset
@admin.register(Server) @admin.register(Server)
@@ -683,17 +683,6 @@ class ACLLinkAdmin(admin.ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) qs = super().get_queryset(request)
qs = qs.select_related('acl__user', 'acl__server') 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 return qs
@admin.display(description='Link', ordering='link') @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 obj.comment[:50] + '...' if len(obj.comment) > 50 else obj.comment
return '-' return '-'
@admin.display(description='Last Access', ordering='last_access') @admin.display(description='Last Access', ordering='last_access_time')
def last_access_display(self, obj): 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 django.utils import timezone
from datetime import timedelta from datetime import timedelta
local_time = localtime(obj.last_access) local_time = localtime(obj.last_access_time)
now = timezone.now() now = timezone.now()
diff = now - obj.last_access diff = now - obj.last_access_time
# Color coding based on age # Color coding based on age
if diff <= timedelta(days=7): if diff <= timedelta(days=7):
@@ -794,17 +783,17 @@ class ACLLinkAdmin(admin.ModelAdmin):
# Add summary statistics to the changelist # Add summary statistics to the changelist
extra_context = extra_context or {} extra_context = extra_context or {}
# Get queryset with annotations for statistics # Get queryset for statistics
queryset = self.get_queryset(request) queryset = self.get_queryset(request)
total_links = queryset.count() 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 django.utils import timezone
from datetime import timedelta from datetime import timedelta
three_months_ago = timezone.now() - timedelta(days=90) three_months_ago = timezone.now() - timedelta(days=90)
old_links = queryset.filter( 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() ).count()
extra_context.update({ extra_context.update({
@@ -817,7 +806,7 @@ class ACLLinkAdmin(admin.ModelAdmin):
def get_ordering(self, request): def get_ordering(self, request):
"""Allow sorting by annotated fields""" """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') order_var = request.GET.get('o')
if order_var: if order_var:
try: try:
@@ -825,9 +814,9 @@ class ACLLinkAdmin(admin.ModelAdmin):
# Check if this corresponds to the last_access column (index 4 in list_display) # 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 field_index == 4: # last_access_display is at index 4
if order_var.startswith('-'): if order_var.startswith('-'):
return ['-last_access'] return ['-last_access_time']
else: else:
return ['last_access'] return ['last_access_time']
except (ValueError, IndexError): except (ValueError, IndexError):
pass pass

View File

@@ -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),
),
]

View File

@@ -130,6 +130,7 @@ class ACLLink(models.Model):
acl = models.ForeignKey(ACL, related_name='links', on_delete=models.CASCADE) 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...") 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") 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): def save(self, *args, **kwargs):
if self.link == "": if self.link == "":

View File

@@ -1,8 +1,7 @@
def userPortal(request, user_hash): def userPortal(request, user_hash):
"""HTML portal for user to view their VPN access links and server information""" """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 import logging
from django.db.models import Count, Q
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -20,78 +19,10 @@ def userPortal(request, user_hash):
# Get all ACL links for the user with server information # Get all ACL links for the user with server information
acl_links = ACLLink.objects.filter(acl__user=user).select_related('acl__server', 'acl') 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 # Group links by server
servers_data = {} servers_data = {}
total_links = 0 total_links = 0
total_connections = 0 # Can be calculated from AccessLog if needed
for link in acl_links: for link in acl_links:
server = link.acl.server server = link.acl.server
@@ -119,34 +50,22 @@ def userPortal(request, user_hash):
'total_connections': 0, '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}" 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({ servers_data[server_name]['links'].append({
'link': link, 'link': link,
'url': link_url, 'url': link_url,
'comment': link.comment or 'Default', 'comment': link.comment or 'Default',
'connections': connection_count, 'last_access': link.last_access_time,
'recent_connections': recent_count,
'daily_usage': daily_usage,
'max_daily': max(daily_usage) if daily_usage else 0,
}) })
servers_data[server_name]['total_connections'] += connection_count
total_links += 1 total_links += 1
# Calculate total recent connections from all links
total_recent_connections = sum(recent_connection_stats.values())
context = { context = {
'user': user, 'user': user,
'servers_data': servers_data, 'servers_data': servers_data,
'total_servers': len(servers_data), 'total_servers': len(servers_data),
'total_links': total_links, 'total_links': total_links,
'total_connections': total_connections,
'recent_connections': total_recent_connections,
'external_address': EXTERNAL_ADDRESS, 'external_address': EXTERNAL_ADDRESS,
} }
@@ -184,6 +103,7 @@ def userFrontend(request, user_hash):
def shadowsocks(request, link): def shadowsocks(request, link):
from .models import ACLLink, AccessLog from .models import ACLLink, AccessLog
import logging import logging
from django.utils import timezone
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -244,6 +164,11 @@ def shadowsocks(request, link):
} }
response = yaml.dump(config, allow_unicode=True) 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) 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' }") return HttpResponse(response, content_type=f"{ 'application/json; charset=utf-8' if request.GET.get('mode') == 'json' else 'text/html' }")