import re from django import forms from django.core.exceptions import ValidationError from ..models import Person class UserCreationForm(forms.Form): """Form für die Erstellung neuer Benutzer""" username = forms.CharField( label="Benutzername", max_length=150, help_text="Eindeutiger Benutzername für die Anmeldung", widget=forms.TextInput(attrs={"class": "form-control"}), ) email = forms.EmailField( label="E-Mail-Adresse", help_text="E-Mail-Adresse des Benutzers", widget=forms.EmailInput(attrs={"class": "form-control"}), ) first_name = forms.CharField( label="Vorname", max_length=30, required=False, widget=forms.TextInput(attrs={"class": "form-control"}), ) last_name = forms.CharField( label="Nachname", max_length=150, required=False, widget=forms.TextInput(attrs={"class": "form-control"}), ) password1 = forms.CharField( label="Passwort", widget=forms.PasswordInput(attrs={"class": "form-control"}), help_text="Mindestens 8 Zeichen", ) password2 = forms.CharField( label="Passwort bestätigen", widget=forms.PasswordInput(attrs={"class": "form-control"}), help_text="Geben Sie das Passwort zur Bestätigung erneut ein", ) is_active = forms.BooleanField( label="Aktiv", required=False, initial=True, help_text="Benutzer kann sich anmelden", widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), ) is_staff = forms.BooleanField( label="Staff-Status", required=False, help_text="Benutzer kann auf Django Admin zugreifen", widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), ) def clean_username(self): username = self.cleaned_data["username"] from django.contrib.auth.models import User if User.objects.filter(username=username).exists(): raise forms.ValidationError( "Ein Benutzer mit diesem Namen existiert bereits." ) return username def clean_email(self): email = self.cleaned_data["email"] from django.contrib.auth.models import User if User.objects.filter(email=email).exists(): raise forms.ValidationError( "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits." ) return email def clean(self): cleaned_data = super().clean() password1 = cleaned_data.get("password1") password2 = cleaned_data.get("password2") if password1 and password2: if password1 != password2: raise forms.ValidationError("Die Passwörter stimmen nicht überein.") if len(password1) < 8: raise forms.ValidationError( "Das Passwort muss mindestens 8 Zeichen lang sein." ) return cleaned_data class UserUpdateForm(forms.ModelForm): """Form für die Bearbeitung bestehender Benutzer""" class Meta: from django.contrib.auth.models import User model = User fields = [ "username", "email", "first_name", "last_name", "is_active", "is_staff", ] widgets = { "username": forms.TextInput(attrs={"class": "form-control"}), "email": forms.EmailInput(attrs={"class": "form-control"}), "first_name": forms.TextInput(attrs={"class": "form-control"}), "last_name": forms.TextInput(attrs={"class": "form-control"}), "is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}), "is_staff": forms.CheckboxInput(attrs={"class": "form-check-input"}), } labels = { "username": "Benutzername", "email": "E-Mail-Adresse", "first_name": "Vorname", "last_name": "Nachname", "is_active": "Aktiv", "is_staff": "Staff-Status", } help_texts = { "username": "Eindeutiger Benutzername für die Anmeldung", "email": "E-Mail-Adresse des Benutzers", "is_active": "Benutzer kann sich anmelden", "is_staff": "Benutzer kann auf Django Admin zugreifen", } class PasswordChangeForm(forms.Form): """Form für Passwort-Änderungen""" new_password1 = forms.CharField( label="Neues Passwort", widget=forms.PasswordInput(attrs={"class": "form-control"}), help_text="Mindestens 8 Zeichen", ) new_password2 = forms.CharField( label="Neues Passwort bestätigen", widget=forms.PasswordInput(attrs={"class": "form-control"}), help_text="Geben Sie das neue Passwort zur Bestätigung erneut ein", ) def clean(self): cleaned_data = super().clean() password1 = cleaned_data.get("new_password1") password2 = cleaned_data.get("new_password2") if password1 and password2: if password1 != password2: raise forms.ValidationError("Die Passwörter stimmen nicht überein.") if len(password1) < 8: raise forms.ValidationError( "Das Passwort muss mindestens 8 Zeichen lang sein." ) return cleaned_data class UserPermissionForm(forms.Form): """Form für die Zuweisung von Berechtigungen""" def __init__(self, *args, **kwargs): user = kwargs.pop("user", None) super().__init__(*args, **kwargs) from django.contrib.auth.models import Permission # Get all custom permissions for stiftung app app_permissions = Permission.objects.filter( content_type__app_label="stiftung" ).order_by("name") # Create checkbox fields for each permission for perm in app_permissions: field_name = f"perm_{perm.id}" self.fields[field_name] = forms.BooleanField( label=perm.name, required=False, widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), ) # Set initial values if user is provided if user: self.fields[field_name].initial = user.has_perm( f"stiftung.{perm.codename}" ) def get_permission_groups(self): """Group permissions by functionality for template rendering""" from django.contrib.auth.models import Permission groups = { "entities": { "name": "Entitäten verwalten", "permissions": [], "icon": "fas fa-users", }, "documents": { "name": "Dokumentenverwaltung", "permissions": [], "icon": "fas fa-folder-open", }, "financial": { "name": "Finanzverwaltung", "permissions": [], "icon": "fas fa-euro-sign", }, "administration": { "name": "Administration", "permissions": [], "icon": "fas fa-cogs", }, "system": {"name": "System", "permissions": [], "icon": "fas fa-server"}, } # Get all permissions to properly categorize them for field_name, field in self.fields.items(): if field_name.startswith("perm_"): # Extract permission ID from field name perm_id = field_name.replace("perm_", "") try: permission = Permission.objects.get(id=perm_id) label = permission.name.lower() codename = permission.codename.lower() # Get bound field for proper template rendering bound_field = self[field_name] # More precise categorization based on both name and codename if ( any( word in codename for word in [ "destinataer", "land", "paechter", "verpachtung", "foerderung", ] ) and "manage_" in codename or "view_" in codename ): groups["entities"]["permissions"].append( (field_name, bound_field, permission) ) elif ( any( word in codename for word in ["documents", "link_documents"] ) or "dokument" in label ): groups["documents"]["permissions"].append( (field_name, bound_field, permission) ) elif any( word in codename for word in [ "verwaltungskosten", "konten", "rentmeister", "approve_payments", ] ) or any( word in label for word in [ "verwaltungskosten", "konto", "rentmeister", "zahlung", ] ): groups["financial"]["permissions"].append( (field_name, bound_field, permission) ) elif any( word in codename for word in [ "administration", "audit", "backup", "manage_users", "manage_permissions", ] ) or any( word in label for word in [ "administration", "audit", "backup", "benutzer", "berechtigung", ] ): groups["administration"]["permissions"].append( (field_name, bound_field, permission) ) else: groups["system"]["permissions"].append( (field_name, bound_field, permission) ) except Permission.DoesNotExist: # Create a fallback permission-like object with proper display class FallbackPermission: def __init__(self, field_name): self.name = field_name.replace('_', ' ').title() self.codename = field_name fallback_perm = FallbackPermission(field_name) bound_field = self[field_name] # Get bound field for exception case too groups["system"]["permissions"].append((field_name, bound_field, fallback_perm)) return groups class TwoFactorSetupForm(forms.Form): """Form for setting up 2FA with TOTP verification""" token = forms.CharField( max_length=6, min_length=6, widget=forms.TextInput(attrs={ 'class': 'form-control text-center', 'placeholder': '000000', 'autocomplete': 'off', 'pattern': '[0-9]{6}', 'inputmode': 'numeric' }), label='Bestätigungscode', help_text='6-stelliger Code aus Ihrer Authenticator-App' ) def clean_token(self): token = self.cleaned_data.get('token') if token and not token.isdigit(): raise ValidationError('Der Code darf nur Zahlen enthalten.') return token class TwoFactorVerifyForm(forms.Form): """Form for verifying 2FA during login""" otp_token = forms.CharField( max_length=8, min_length=6, widget=forms.TextInput(attrs={ 'class': 'form-control form-control-lg text-center', 'placeholder': '000000', 'autocomplete': 'off', 'autofocus': True }), label='Authentifizierungscode', help_text='6-stelliger Code aus der App oder 8-stelliger Backup-Code' ) def clean_otp_token(self): token = self.cleaned_data.get('otp_token') if token: token = token.strip().lower() # Allow 6-digit TOTP codes or 8-character backup codes if len(token) == 6 and token.isdigit(): return token elif len(token) == 8 and re.match(r'^[a-f0-9]{8}$', token): return token else: raise ValidationError( 'Bitte geben Sie einen 6-stelligen Authenticator-Code oder 8-stelligen Backup-Code ein.' ) return token class TwoFactorDisableForm(forms.Form): """Form for disabling 2FA with password confirmation""" password = forms.CharField( widget=forms.PasswordInput(attrs={ 'class': 'form-control', 'autocomplete': 'current-password', 'autofocus': True }), label='Passwort', help_text='Geben Sie Ihr aktuelles Passwort zur Bestätigung ein' ) class BackupTokenRegenerateForm(forms.Form): """Form for regenerating backup tokens""" password = forms.CharField( widget=forms.PasswordInput(attrs={ 'class': 'form-control', 'autocomplete': 'current-password' }), label='Passwort', help_text='Geben Sie Ihr Passwort ein, um neue Backup-Codes zu generieren' ) class PersonForm(forms.ModelForm): """Form für das Erstellen und Bearbeiten von Personen (Legacy)""" class Meta: model = Person fields = [ "familienzweig", "vorname", "nachname", "geburtsdatum", "email", "telefon", "iban", "adresse", "notizen", "aktiv", ] widgets = { "familienzweig": forms.Select(attrs={"class": "form-select"}), "vorname": forms.TextInput(attrs={"class": "form-control"}), "nachname": forms.TextInput(attrs={"class": "form-control"}), "geburtsdatum": forms.DateInput( attrs={"class": "form-control", "type": "date"} ), "email": forms.EmailInput(attrs={"class": "form-control"}), "telefon": forms.TextInput(attrs={"class": "form-control"}), "iban": forms.TextInput( attrs={"class": "form-control", "placeholder": "DE89370400440532013000"} ), "adresse": forms.Textarea(attrs={"class": "form-control", "rows": 3}), "notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}), "aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}), } labels = { "familienzweig": "Familienzweig", "vorname": "Vorname *", "nachname": "Nachname *", "geburtsdatum": "Geburtsdatum", "email": "E-Mail", "telefon": "Telefon", "iban": "IBAN", "adresse": "Adresse", "notizen": "Notizen", "aktiv": "Aktiv", } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Markiere Pflichtfelder self.fields["vorname"].required = True self.fields["nachname"].required = True