feat: Implement group-restricted Live Sessions & IP Monitoring and 1-click Admin Email Confirmation

This commit is contained in:
Richard
2026-08-01 12:26:44 +02:00
parent 4b13e57fcb
commit 7614163f78
15 changed files with 943 additions and 665 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16 -1
View File
@@ -1,6 +1,15 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User, RegistrationDomainRule, RegistrationRestrictionSetting
from django.contrib.auth.models import Group
from .models import User, RegistrationDomainRule, RegistrationRestrictionSetting, UserActivity
SESSION_MONITOR_GROUP = "Session Monitor"
# Ensure "Session Monitor" group exists in Django Admin
try:
Group.objects.get_or_create(name=SESSION_MONITOR_GROUP)
except Exception:
pass
@admin.register(User)
class UserAdmin(BaseUserAdmin):
@@ -18,3 +27,9 @@ class RegistrationDomainRuleAdmin(admin.ModelAdmin):
@admin.register(RegistrationRestrictionSetting)
class RegistrationRestrictionSettingAdmin(admin.ModelAdmin):
list_display = ['is_restriction_enabled']
@admin.register(UserActivity)
class UserActivityAdmin(admin.ModelAdmin):
list_display = ['user', 'guest_name', 'ip_address', 'last_path', 'last_seen']
list_filter = ['last_seen']
search_fields = ['ip_address', 'guest_name', 'user__username', 'user__email', 'user__display_name', 'last_path']
+44
View File
@@ -0,0 +1,44 @@
from .models import UserActivity
class UserActivityMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# Record activity for non-static requests
path = request.path
if not path.startswith('/static/') and not path.startswith('/media/'):
try:
# Ensure session exists
if not request.session.session_key:
request.session.create()
session_key = request.session.session_key
# Extract Client IP handling reverse proxies (Traefik / Nginx)
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
else:
ip = request.META.get('REMOTE_ADDR', '')
user_agent = request.META.get('HTTP_USER_AGENT', '')[:245]
user = request.user if request.user.is_authenticated else None
guest_name = request.session.get('guest_name', '') or request.COOKIES.get('guest_display_name', '')
if session_key:
UserActivity.objects.update_or_create(
session_key=session_key,
defaults={
'user': user,
'guest_name': guest_name if not user else '',
'ip_address': ip,
'user_agent': user_agent,
'last_path': path[:245],
}
)
except Exception:
pass
return response
@@ -0,0 +1,33 @@
# Generated by Django 5.2.16 on 2026-08-01 10:25
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0006_registrationrestrictionsetting_require_email_verification'),
]
operations = [
migrations.CreateModel(
name='UserActivity',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('guest_name', models.CharField(blank=True, max_length=150, null=True)),
('session_key', models.CharField(db_index=True, max_length=255)),
('ip_address', models.CharField(blank=True, default='', max_length=50)),
('user_agent', models.CharField(blank=True, default='', max_length=255)),
('last_path', models.CharField(blank=True, default='', max_length=255)),
('last_seen', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='activities', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Benutzer-Aktivität / Sitzung',
'verbose_name_plural': 'Benutzer-Aktivitäten / Sitzungen',
'ordering': ['-last_seen'],
},
),
]
+18
View File
@@ -86,3 +86,21 @@ class GuestClaimToken(models.Model):
def __str__(self):
return f"Claim Token for {self.guest_name} ({self.token})"
class UserActivity(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='activities')
guest_name = models.CharField(max_length=150, null=True, blank=True)
session_key = models.CharField(max_length=255, db_index=True)
ip_address = models.CharField(max_length=50, blank=True, default='')
user_agent = models.CharField(max_length=255, blank=True, default='')
last_path = models.CharField(max_length=255, blank=True, default='')
last_seen = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-last_seen']
verbose_name = 'Benutzer-Aktivität / Sitzung'
verbose_name_plural = 'Benutzer-Aktivitäten / Sitzungen'
def __str__(self):
name = self.user.get_display_name() if self.user else (self.guest_name or "Gast")
return f"{name} ({self.ip_address}) - {self.last_seen}"
+2 -1
View File
@@ -5,7 +5,7 @@ from .views import (
RegistrationDomainRuleViewSet, RegistrationRestrictionSettingView,
PendingUsersView, ApproveUserView, AdminUserViewSet,
GenerateClaimLinkView, ClaimInfoView,
SMTPSettingView, TestSMTPEmailView, UserListView
SMTPSettingView, TestSMTPEmailView, UserListView, ActiveSessionsView
)
router = DefaultRouter()
@@ -18,6 +18,7 @@ urlpatterns = [
path('login/', LoginView.as_view(), name='login'),
path('me/', MeView.as_view(), name='me'),
path('all/', UserListView.as_view(), name='all-users'),
path('active-sessions/', ActiveSessionsView.as_view(), name='active-sessions'),
path('restriction-setting/', RegistrationRestrictionSettingView.as_view(), name='restriction-setting'),
path('smtp-setting/', SMTPSettingView.as_view(), name='smtp-setting'),
path('smtp-setting/test/', TestSMTPEmailView.as_view(), name='smtp-setting-test'),
+67 -1
View File
@@ -1,10 +1,14 @@
from rest_framework import views, viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from django.contrib.auth import authenticate, get_user_model
from django.shortcuts import get_object_or_404
from django.core.mail import get_connection, EmailMultiAlternatives
from .models import RegistrationDomainRule, RegistrationRestrictionSetting, GuestClaimToken, EmailVerificationToken, SMTPSetting
from .models import (
RegistrationDomainRule, RegistrationRestrictionSetting, GuestClaimToken,
EmailVerificationToken, SMTPSetting, UserActivity
)
from .serializers import (
UserSerializer, AdminUserSerializer, RegisterSerializer,
RegistrationDomainRuleSerializer, RegistrationRestrictionSettingSerializer, SMTPSettingSerializer
@@ -80,6 +84,17 @@ class AdminUserViewSet(viewsets.ModelViewSet):
user_to_delete.delete()
return Response({'message': f'Benutzer "{name}" wurde erfolgreich gelöscht.'})
@action(detail=True, methods=['post'], url_path='confirm-email')
def confirm_email(self, request, pk=None):
target_user = self.get_object()
target_user.is_email_verified = True
target_user.is_approved = True
target_user.save()
EmailVerificationToken.objects.filter(user=target_user).update(is_used=True)
return Response({
'message': f'E-Mail-Adresse von {target_user.get_display_name()} ({target_user.email}) wurde manuell von dir als bestätigt markiert.'
})
class RegisterView(views.APIView):
permission_classes = [permissions.AllowAny]
@@ -406,3 +421,54 @@ class UserListView(views.APIView):
def get(self, request):
users = User.objects.filter(is_active=True).order_by('username')
return Response(UserSerializer(users, many=True).data)
SESSION_MONITOR_GROUP = "Session Monitor"
class IsSessionMonitorGroupOrSuperuser(permissions.BasePermission):
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
if request.user.is_superuser:
return True
return request.user.groups.filter(name=SESSION_MONITOR_GROUP).exists()
class ActiveSessionsView(views.APIView):
permission_classes = [IsSessionMonitorGroupOrSuperuser]
def get(self, request):
from datetime import timedelta
from django.utils import timezone
cutoff = timezone.now() - timedelta(hours=24)
activities = UserActivity.objects.filter(last_seen__gte=cutoff).select_related('user').order_by('-last_seen')
data = []
for act in activities:
user_info = None
if act.user:
user_info = {
'id': act.user.id,
'username': act.user.username,
'email': act.user.email,
'display_name': act.user.get_display_name(),
'is_admin': act.user.is_admin_user or act.user.is_staff or act.user.is_superuser,
}
data.append({
'id': act.id,
'user': user_info,
'guest_name': act.guest_name if not act.user else None,
'display_name': act.user.get_display_name() if act.user else (act.guest_name or "Unangemeldeter Gast"),
'session_key': act.session_key[:12] + '...',
'ip_address': act.ip_address or '127.0.0.1',
'user_agent': act.user_agent,
'last_path': act.last_path,
'last_seen': act.last_seen.isoformat(),
})
return Response({
'group_name': SESSION_MONITOR_GROUP,
'is_authorized': True,
'count': len(data),
'sessions': data
})