Files

475 lines
20 KiB
Python

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, UserActivity
)
from .serializers import (
UserSerializer, AdminUserSerializer, RegisterSerializer,
RegistrationDomainRuleSerializer, RegistrationRestrictionSettingSerializer, SMTPSettingSerializer
)
from apps.events.models import ShiftSignup
User = get_user_model()
def send_verification_email_to_user(user, v_token_str, request_host=None):
smtp = SMTPSetting.get_solo()
host_str = request_host or 'localhost:3000'
verify_url = f"http://{host_str}/?verify_email={v_token_str}"
subject = "E-Mail-Adresse bestätigen — Veranstaltungsschichtplaner"
text_content = f"Hallo {user.get_display_name()},\n\nbitte bestätige deine E-Mail-Adresse durch Aufruf des folgenden Links:\n{verify_url}\n\nVielen Dank!"
html_content = f"""
<div style="font-family: sans-serif; padding: 20px; line-height: 1.6; color: #1f2937;">
<h2 style="color: #4f46e5;">Willkommen beim Schichtplaner!</h2>
<p>Hallo <strong>{user.get_display_name()}</strong>,</p>
<p>vielen Dank für deine Registrierung. Bitte bestätige deine E-Mail-Adresse durch Klick auf den folgenden Button:</p>
<p style="margin: 25px 0;">
<a href="{verify_url}" style="background: #4f46e5; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold; display: inline-block;">
E-Mail-Adresse Bestätigen
</a>
</p>
<p style="color: #6b7280; font-size: 12px;">Oder kopiere diesen Link in deinen Browser:<br><a href="{verify_url}">{verify_url}</a></p>
</div>
"""
print("==================================================")
print(f"[E-MAIL BESTÄTIGUNG]: {verify_url}")
print("==================================================")
if smtp.is_active and smtp.host:
try:
connection = get_connection(
host=smtp.host,
port=smtp.port,
username=smtp.username,
password=smtp.password,
use_tls=smtp.use_tls,
use_ssl=smtp.use_ssl,
fail_silently=False,
)
msg = EmailMultiAlternatives(
subject=subject,
body=text_content,
from_email=smtp.from_email or 'noreply@schichtplaner.local',
to=[user.email],
connection=connection
)
msg.attach_alternative(html_content, "text/html")
msg.send()
return True, "E-Mail erfolgreich versendet."
except Exception as e:
return False, f"SMTP-Fehler: {str(e)}"
return True, "Bestätigungslink in Konsole ausgegeben."
class AdminUserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all().order_by('-date_joined')
serializer_class = AdminUserSerializer
permission_classes = [permissions.IsAdminUser]
def destroy(self, request, *args, **kwargs):
user_to_delete = self.get_object()
if user_to_delete.id == request.user.id:
return Response(
{'error': 'Du kannst dein eigenes Admin-Konto nicht selbst löschen.'},
status=status.HTTP_400_BAD_REQUEST
)
name = user_to_delete.get_display_name()
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]
def post(self, request):
serializer = RegisterSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
user.is_approved = False
user.is_email_verified = False
user.is_active = True
user.save()
setting = RegistrationRestrictionSetting.get_solo()
claim_token_str = request.data.get('claim_token')
claimed_target_name = None
if claim_token_str:
try:
claim_obj = GuestClaimToken.objects.get(token=claim_token_str, is_claimed=False)
claim_obj.is_claimed = True
claim_obj.save()
# Assign target signup
target_signup = claim_obj.signup
if target_signup and not target_signup.user:
target_signup.user = user
target_signup.guest_name = None
target_signup.guest_session_key = None
target_signup.save()
claimed_target_name = claim_obj.guest_name
except Exception:
pass
# Reassign any guest shift signups to this newly created user!
session_key = request.session.session_key
display_name = user.get_display_name()
guest_signups = ShiftSignup.objects.filter(user=None)
assigned_count = 0
for signup in guest_signups:
matches_session = session_key and signup.guest_session_key == session_key
matches_display = signup.guest_name and signup.guest_name.strip().lower() == display_name.strip().lower()
matches_claimed_name = claimed_target_name and signup.guest_name and signup.guest_name.strip().lower() == claimed_target_name.strip().lower()
if matches_session or matches_display or matches_claimed_name:
signup.user = user
signup.guest_name = None
signup.guest_session_key = None
signup.save()
assigned_count += 1
if setting.require_email_verification:
user.is_approved = False
user.is_email_verified = False
user.is_active = True
user.save()
v_token = EmailVerificationToken.objects.create(user=user)
req_host = request.get_host()
send_verification_email_to_user(user, str(v_token.token), request_host=req_host)
return Response({
'requires_verification': True,
'verification_token': str(v_token.token),
'claimed_shifts_count': assigned_count,
'message': 'Konto erfolgreich erstellt! Bitte bestätige deine E-Mail-Adresse über den Link in der Bestätigungs-E-Mail oder warte auf die Admin-Freischaltung.'
}, status=status.HTTP_201_CREATED)
# Opt-Out: Email verification not required!
user.is_email_verified = True
if setting.require_admin_approval:
user.is_approved = False
user.is_active = False
user.save()
return Response({
'requires_approval': True,
'claimed_shifts_count': assigned_count,
'message': 'Konto erfolgreich registriert! Ein Administrator muss dein Konto vor der ersten Anmeldung freischalten.'
}, status=status.HTTP_201_CREATED)
user.is_approved = True
user.is_active = True
user.save()
token, _ = Token.objects.get_or_create(user=user)
user_data = UserSerializer(user).data
return Response({
'token': token.key,
'user': user_data,
'claimed_shifts_count': assigned_count,
'message': f'Konto erfolgreich erstellt! {assigned_count} Gast-Schichten wurden deinem Konto zugewiesen.' if assigned_count > 0 else 'Konto erfolgreich erstellt!'
}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class VerifyEmailView(views.APIView):
permission_classes = [permissions.AllowAny]
def post(self, request):
token_str = request.data.get('token')
if not token_str:
return Response({'error': 'Token erforderlich.'}, status=status.HTTP_400_BAD_REQUEST)
v_token = EmailVerificationToken.objects.filter(token=token_str, is_used=False).first()
if not v_token:
return Response({'error': 'Ungültiger oder bereits verwendeter Bestätigungslink.'}, status=status.HTTP_400_BAD_REQUEST)
v_token.is_used = True
v_token.save()
user = v_token.user
user.is_email_verified = True
user.is_approved = True
user.is_active = True
user.save()
token, _ = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
'user': UserSerializer(user).data,
'message': f'E-Mail-Adresse für {user.get_display_name()} wurde erfolgreich bestätigt! Du bist jetzt angemeldet.'
}, status=status.HTTP_200_OK)
class GenerateClaimLinkView(views.APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request, signup_id):
signup = get_object_or_404(ShiftSignup, pk=signup_id)
if signup.user:
return Response({'error': 'Diese Schicht ist bereits mit einem festen Benutzerkonto verknüpft.'}, status=status.HTTP_400_BAD_REQUEST)
guest_name = signup.guest_name or "Gast"
claim_obj, _ = GuestClaimToken.objects.get_or_create(
signup=signup,
created_by=request.user,
defaults={'guest_name': guest_name}
)
return Response({
'token': str(claim_obj.token),
'guest_name': guest_name,
'event_title': signup.shift.task_area.event.title,
'shift_title': signup.shift.title,
'created_by': request.user.get_display_name()
})
class ClaimInfoView(views.APIView):
permission_classes = [permissions.AllowAny]
def get(self, request, token_uuid):
claim_obj = get_object_or_404(GuestClaimToken, token=token_uuid)
return Response({
'token': str(claim_obj.token),
'guest_name': claim_obj.guest_name,
'is_claimed': claim_obj.is_claimed,
'event_title': claim_obj.signup.shift.task_area.event.title if claim_obj.signup else 'Veranstaltung',
'shift_title': claim_obj.signup.shift.title if claim_obj.signup else 'Schicht',
'created_by': claim_obj.created_by.get_display_name()
})
class LoginView(views.APIView):
permission_classes = [permissions.AllowAny]
def post(self, request):
username = request.data.get('username')
password = request.data.get('password')
if not username or not password:
return Response({'error': 'Benutzername und Passwort erforderlich.'}, status=status.HTTP_400_BAD_REQUEST)
user_obj = None
if '@' in username:
try:
user_obj = User.objects.get(email__iexact=username)
username = user_obj.username
except User.DoesNotExist:
pass
else:
try:
user_obj = User.objects.get(username__iexact=username)
except User.DoesNotExist:
pass
if user_obj:
if not (user_obj.is_approved or user_obj.is_email_verified or user_obj.is_superuser or user_obj.is_admin_user):
return Response({
'error': 'Anmeldung nicht möglich: Deine E-Mail-Adresse wurde noch nicht bestätigt und dein Konto wurde noch nicht vom Admin freigeschaltet.'
}, status=status.HTTP_403_FORBIDDEN)
user = authenticate(username=username, password=password)
if not user:
return Response({'error': 'Ungültige Anmeldedaten.'}, status=status.HTTP_401_UNAUTHORIZED)
token, _ = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
'user': UserSerializer(user).data
})
class MeView(views.APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
return Response(UserSerializer(request.user).data)
def patch(self, request):
serializer = UserSerializer(request.user, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RegistrationDomainRuleViewSet(viewsets.ModelViewSet):
queryset = RegistrationDomainRule.objects.all()
serializer_class = RegistrationDomainRuleSerializer
permission_classes = [permissions.IsAdminUser]
class RegistrationRestrictionSettingView(views.APIView):
def get_permissions(self):
if self.request.method == 'GET':
return [permissions.AllowAny()]
return [permissions.IsAdminUser()]
def get(self, request):
setting = RegistrationRestrictionSetting.get_solo()
active_domains = list(RegistrationDomainRule.objects.filter(is_active=True).values_list('domain', flat=True))
return Response({
'is_restriction_enabled': setting.is_restriction_enabled,
'require_admin_approval': setting.require_admin_approval,
'require_email_verification': setting.require_email_verification,
'active_domains': active_domains
})
def post(self, request):
setting = RegistrationRestrictionSetting.get_solo()
if 'is_restriction_enabled' in request.data:
setting.is_restriction_enabled = request.data['is_restriction_enabled']
if 'require_admin_approval' in request.data:
setting.require_admin_approval = request.data['require_admin_approval']
if 'require_email_verification' in request.data:
setting.require_email_verification = request.data['require_email_verification']
setting.save()
return Response({
'is_restriction_enabled': setting.is_restriction_enabled,
'require_admin_approval': setting.require_admin_approval,
'require_email_verification': setting.require_email_verification
})
class SMTPSettingView(views.APIView):
permission_classes = [permissions.IsAdminUser]
def get(self, request):
setting = SMTPSetting.get_solo()
return Response(SMTPSettingSerializer(setting).data)
def post(self, request):
setting = SMTPSetting.get_solo()
serializer = SMTPSettingSerializer(setting, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class TestSMTPEmailView(views.APIView):
permission_classes = [permissions.IsAdminUser]
def post(self, request):
smtp = SMTPSetting.get_solo()
target_email = request.data.get('email') or request.user.email
if not target_email:
return Response({'error': 'E-Mail-Adresse erforderlich.'}, status=status.HTTP_400_BAD_REQUEST)
try:
connection = get_connection(
host=smtp.host,
port=smtp.port,
username=smtp.username,
password=smtp.password,
use_tls=smtp.use_tls,
use_ssl=smtp.use_ssl,
fail_silently=False,
)
msg = EmailMultiAlternatives(
subject="Test E-Mail — SMTP Konfiguration",
body=f"Hallo,\n\ndies ist eine Test-E-Mail zur Überprüfung der SMTP-Einstellungen für {request.user.get_display_name()}.",
from_email=smtp.from_email or 'noreply@schichtplaner.local',
to=[target_email],
connection=connection
)
msg.send()
return Response({'message': f'Test E-Mail erfolgreich an {target_email} gesendet!'})
except Exception as e:
return Response({'error': f'SMTP-Verbindungsfehler: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
class PendingUsersView(views.APIView):
permission_classes = [permissions.IsAdminUser]
def get(self, request):
pending_users = User.objects.filter(is_approved=False, is_email_verified=False, is_superuser=False).order_by('-date_joined')
return Response(UserSerializer(pending_users, many=True).data)
class ApproveUserView(views.APIView):
permission_classes = [permissions.IsAdminUser]
def post(self, request, user_id):
user = get_object_or_404(User, pk=user_id)
user.is_approved = True
user.is_active = True
user.save()
return Response({'message': f'Nutzer {user.get_display_name()} wurde erfolgreich freigeschaltet.'})
def delete(self, request, user_id):
user = get_object_or_404(User, pk=user_id)
name = user.get_display_name()
user.delete()
return Response({'message': f'Registrierung von {name} wurde abgelehnt und gelöscht.'})
class UserListView(views.APIView):
permission_classes = [permissions.IsAuthenticated]
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
})