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"""

Willkommen beim Schichtplaner!

Hallo {user.get_display_name()},

vielen Dank für deine Registrierung. Bitte bestätige deine E-Mail-Adresse durch Klick auf den folgenden Button:

E-Mail-Adresse Bestätigen

Oder kopiere diesen Link in deinen Browser:
{verify_url}

""" 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 })