snapshot: preserve role-aware notification preferences and operational alerts
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from django import forms
|
||||
from pathlib import Path
|
||||
from datetime import timedelta
|
||||
from django.contrib.auth import get_user_model, password_validation
|
||||
from django.contrib.auth import authenticate, get_user_model, password_validation
|
||||
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, PasswordResetForm, SetPasswordForm
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
@@ -10,7 +10,7 @@ from django.utils.translation import get_language, gettext as _, gettext_lazy
|
||||
from .branding import get_company_email_domain
|
||||
from .form_builder import apply_form_field_config
|
||||
from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, PortalCompanyConfig, PortalTrialConfig, UserProfile, WorkflowConfig
|
||||
from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role
|
||||
from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role, user_has_capability
|
||||
from .totp import normalize_recovery_code, normalize_totp_token, verify_totp_token
|
||||
|
||||
|
||||
@@ -102,9 +102,41 @@ HARDWARE_EXTRA_CHOICES = [('Smartphone', 'Smartphone'), ('Anderes', 'Anderes')]
|
||||
SOFTWARE_EXTRA_CHOICES = [('Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)', 'Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)'), ('Anderes', 'Anderes')]
|
||||
|
||||
|
||||
class AppAuthenticationForm(AuthenticationForm):
|
||||
class AppLoginForm(forms.Form):
|
||||
username = forms.CharField(label=gettext_lazy('Benutzername'))
|
||||
password = forms.CharField(label=gettext_lazy('Passwort'), strip=False, widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}))
|
||||
password = forms.CharField(
|
||||
label=gettext_lazy('Passwort'),
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
|
||||
)
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': gettext_lazy('Benutzername oder Passwort sind nicht korrekt.'),
|
||||
'inactive': gettext_lazy('Dieses Konto ist deaktiviert.'),
|
||||
}
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
self.request = request
|
||||
self.user_cache = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
username = cleaned_data.get('username')
|
||||
password = cleaned_data.get('password')
|
||||
if username and password:
|
||||
self.user_cache = authenticate(self.request, username=username, password=password)
|
||||
if self.user_cache is None:
|
||||
raise ValidationError(self.error_messages['invalid_login'], code='invalid_login')
|
||||
if not self.user_cache.is_active:
|
||||
raise ValidationError(self.error_messages['inactive'], code='inactive')
|
||||
return cleaned_data
|
||||
|
||||
def get_user(self):
|
||||
return self.user_cache
|
||||
|
||||
|
||||
class AppTOTPChallengeForm(forms.Form):
|
||||
otp_code = forms.CharField(
|
||||
label=gettext_lazy('TOTP-Code'),
|
||||
required=False,
|
||||
@@ -119,37 +151,30 @@ class AppAuthenticationForm(AuthenticationForm):
|
||||
)
|
||||
|
||||
error_messages = {
|
||||
**AuthenticationForm.error_messages,
|
||||
'invalid_otp': gettext_lazy('Der TOTP-Code ist ungültig.'),
|
||||
'missing_otp': gettext_lazy('Bitte geben Sie Ihren TOTP-Code ein.'),
|
||||
}
|
||||
|
||||
def __init__(self, *args, profile=None, **kwargs):
|
||||
self.profile = profile
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
user = self.get_user()
|
||||
if not user:
|
||||
profile = self.profile
|
||||
if not profile or not profile.totp_enabled:
|
||||
return cleaned_data
|
||||
profile, _ = UserProfile.objects.get_or_create(user=user)
|
||||
if profile.totp_enabled:
|
||||
otp_code = normalize_totp_token(cleaned_data.get('otp_code'))
|
||||
recovery_code = normalize_recovery_code(cleaned_data.get('recovery_code'))
|
||||
if recovery_code:
|
||||
if not profile.consume_recovery_code(recovery_code):
|
||||
raise ValidationError(
|
||||
self.error_messages['invalid_otp'],
|
||||
code='invalid_otp',
|
||||
)
|
||||
return cleaned_data
|
||||
if not otp_code:
|
||||
raise ValidationError(
|
||||
self.error_messages['missing_otp'],
|
||||
code='missing_otp',
|
||||
)
|
||||
if not profile.totp_secret or not verify_totp_token(profile.totp_secret, otp_code, for_time=int(timezone.now().timestamp())):
|
||||
raise ValidationError(
|
||||
self.error_messages['invalid_otp'],
|
||||
code='invalid_otp',
|
||||
)
|
||||
|
||||
otp_code = normalize_totp_token(cleaned_data.get('otp_code'))
|
||||
recovery_code = normalize_recovery_code(cleaned_data.get('recovery_code'))
|
||||
if recovery_code:
|
||||
if not profile.consume_recovery_code(recovery_code):
|
||||
raise ValidationError(self.error_messages['invalid_otp'], code='invalid_otp')
|
||||
return cleaned_data
|
||||
if not otp_code:
|
||||
raise ValidationError(self.error_messages['missing_otp'], code='missing_otp')
|
||||
if not profile.totp_secret or not verify_totp_token(profile.totp_secret, otp_code, for_time=int(timezone.now().timestamp())):
|
||||
raise ValidationError(self.error_messages['invalid_otp'], code='invalid_otp')
|
||||
return cleaned_data
|
||||
|
||||
|
||||
@@ -307,18 +332,6 @@ class AccountTOTPDisableForm(forms.Form):
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
|
||||
)
|
||||
verification_code = forms.CharField(
|
||||
label=gettext_lazy('TOTP-Code'),
|
||||
max_length=12,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'autocomplete': 'one-time-code', 'inputmode': 'numeric'}),
|
||||
)
|
||||
recovery_code = forms.CharField(
|
||||
label=gettext_lazy('Recovery-Code'),
|
||||
max_length=32,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'autocomplete': 'one-time-code'}),
|
||||
)
|
||||
|
||||
def __init__(self, *args, user=None, profile=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -333,26 +346,12 @@ class AccountTOTPDisableForm(forms.Form):
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
code = normalize_totp_token(cleaned_data.get('verification_code'))
|
||||
recovery_code = normalize_recovery_code(cleaned_data.get('recovery_code'))
|
||||
if not code and not recovery_code:
|
||||
raise ValidationError(_('Bitte geben Sie einen gültigen TOTP-Code oder Recovery-Code ein.'))
|
||||
secret = getattr(self.profile, 'totp_secret', '') or ''
|
||||
if code:
|
||||
if not secret or not verify_totp_token(secret, code, for_time=int(timezone.now().timestamp())):
|
||||
raise ValidationError(_('Der TOTP-Code ist ungültig.'))
|
||||
return cleaned_data
|
||||
if not self.profile.consume_recovery_code(recovery_code):
|
||||
raise ValidationError(_('Der Recovery-Code ist ungültig.'))
|
||||
if not self.profile or not self.profile.totp_enabled:
|
||||
raise ValidationError(_('TOTP ist für dieses Konto nicht aktiv.'))
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class AccountTOTPRegenerateRecoveryCodesForm(forms.Form):
|
||||
current_password = forms.CharField(
|
||||
label=gettext_lazy('Aktuelles Passwort'),
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
|
||||
)
|
||||
verification_code = forms.CharField(
|
||||
label=gettext_lazy('TOTP-Code'),
|
||||
max_length=12,
|
||||
@@ -371,12 +370,6 @@ class AccountTOTPRegenerateRecoveryCodesForm(forms.Form):
|
||||
self.user = user
|
||||
self.profile = profile
|
||||
|
||||
def clean_current_password(self):
|
||||
password = self.cleaned_data.get('current_password') or ''
|
||||
if not self.user or not self.user.check_password(password):
|
||||
raise ValidationError(_('Das aktuelle Passwort ist nicht korrekt.'))
|
||||
return password
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
code = normalize_totp_token(cleaned_data.get('verification_code'))
|
||||
@@ -393,6 +386,87 @@ class AccountTOTPRegenerateRecoveryCodesForm(forms.Form):
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class AccountNotificationPreferencesForm(forms.Form):
|
||||
onboarding_success = forms.BooleanField(label=gettext_lazy('Onboarding erfolgreich'), required=False)
|
||||
onboarding_failure = forms.BooleanField(label=gettext_lazy('Onboarding fehlgeschlagen'), required=False)
|
||||
offboarding_success = forms.BooleanField(label=gettext_lazy('Offboarding erfolgreich'), required=False)
|
||||
offboarding_failure = forms.BooleanField(label=gettext_lazy('Offboarding fehlgeschlagen'), required=False)
|
||||
backup_success = forms.BooleanField(label=gettext_lazy('Backup erfolgreich'), required=False)
|
||||
backup_failure = forms.BooleanField(label=gettext_lazy('Backup fehlgeschlagen'), required=False)
|
||||
welcome_email_success = forms.BooleanField(label=gettext_lazy('Welcome E-Mail erfolgreich'), required=False)
|
||||
welcome_email_failure = forms.BooleanField(label=gettext_lazy('Welcome E-Mail fehlgeschlagen'), required=False)
|
||||
trial_alerts = forms.BooleanField(label=gettext_lazy('Trial-Hinweise'), required=False)
|
||||
system_alerts = forms.BooleanField(label=gettext_lazy('System-Hinweise'), required=False)
|
||||
|
||||
FIELD_TO_EVENT = {
|
||||
'onboarding_success': UserProfile.NOTIFICATION_ONBOARDING_SUCCESS,
|
||||
'onboarding_failure': UserProfile.NOTIFICATION_ONBOARDING_FAILURE,
|
||||
'offboarding_success': UserProfile.NOTIFICATION_OFFBOARDING_SUCCESS,
|
||||
'offboarding_failure': UserProfile.NOTIFICATION_OFFBOARDING_FAILURE,
|
||||
'backup_success': UserProfile.NOTIFICATION_BACKUP_SUCCESS,
|
||||
'backup_failure': UserProfile.NOTIFICATION_BACKUP_FAILURE,
|
||||
'welcome_email_success': UserProfile.NOTIFICATION_WELCOME_EMAIL_SUCCESS,
|
||||
'welcome_email_failure': UserProfile.NOTIFICATION_WELCOME_EMAIL_FAILURE,
|
||||
'trial_alerts': UserProfile.NOTIFICATION_TRIAL_ALERTS,
|
||||
'system_alerts': UserProfile.NOTIFICATION_SYSTEM_ALERTS,
|
||||
}
|
||||
|
||||
GROUPS = [
|
||||
('workflow', gettext_lazy('Workflow'), ['onboarding_success', 'onboarding_failure', 'offboarding_success', 'offboarding_failure']),
|
||||
('welcome', gettext_lazy('Welcome E-Mail'), ['welcome_email_success', 'welcome_email_failure']),
|
||||
('operations', gettext_lazy('Operations'), ['backup_success', 'backup_failure', 'system_alerts']),
|
||||
('platform', gettext_lazy('Platform'), ['trial_alerts']),
|
||||
]
|
||||
|
||||
def __init__(self, *args, profile=None, user=None, **kwargs):
|
||||
self.profile = profile
|
||||
self.user = user
|
||||
initial = kwargs.setdefault('initial', {})
|
||||
if profile is not None and not args:
|
||||
prefs = profile.get_notification_preferences()
|
||||
for field_name, event_key in self.FIELD_TO_EVENT.items():
|
||||
initial.setdefault(field_name, prefs.get(event_key, True))
|
||||
super().__init__(*args, **kwargs)
|
||||
self.visible_field_names = self._compute_visible_field_names()
|
||||
for field_name in list(self.fields.keys()):
|
||||
if field_name not in self.visible_field_names:
|
||||
self.fields.pop(field_name)
|
||||
|
||||
def _compute_visible_field_names(self) -> list[str]:
|
||||
visible = [
|
||||
'onboarding_success',
|
||||
'onboarding_failure',
|
||||
'offboarding_success',
|
||||
'offboarding_failure',
|
||||
'welcome_email_success',
|
||||
'welcome_email_failure',
|
||||
]
|
||||
if user_has_capability(self.user, 'manage_backups'):
|
||||
visible.extend(['backup_success', 'backup_failure'])
|
||||
if user_has_capability(self.user, 'manage_integrations'):
|
||||
visible.append('system_alerts')
|
||||
if user_has_capability(self.user, 'manage_trial_lifecycle'):
|
||||
visible.append('trial_alerts')
|
||||
return visible
|
||||
|
||||
def grouped_fields(self):
|
||||
groups = []
|
||||
for key, label, field_names in self.GROUPS:
|
||||
rows = [self[name] for name in field_names if name in self.fields]
|
||||
if rows:
|
||||
groups.append({'key': key, 'label': label, 'fields': rows})
|
||||
return groups
|
||||
|
||||
def save(self):
|
||||
prefs = self.profile.get_notification_preferences()
|
||||
for field_name in self.visible_field_names:
|
||||
event_key = self.FIELD_TO_EVENT[field_name]
|
||||
prefs[event_key] = bool(self.cleaned_data.get(field_name))
|
||||
self.profile.notification_preferences = prefs
|
||||
self.profile.save(update_fields=['notification_preferences', 'updated_at'])
|
||||
return self.profile
|
||||
|
||||
|
||||
class UserManagementCreateForm(forms.Form):
|
||||
first_name = forms.CharField(label=_('Vorname'), max_length=150, required=False)
|
||||
last_name = forms.CharField(label=_('Nachname'), max_length=150, required=False)
|
||||
@@ -604,7 +678,7 @@ class OnboardingRequestForm(forms.ModelForm):
|
||||
department = forms.ChoiceField(label='Abteilung', choices=DEPARTMENT_CHOICES, required=True)
|
||||
work_email = forms.EmailField(
|
||||
label='Gewünschte dienstliche E-Mail-Adresse',
|
||||
help_text='Bitte nutzen Sie das Format name@tub.co.',
|
||||
help_text='',
|
||||
)
|
||||
contract_start = forms.DateField(label='Vertragsbeginn', widget=forms.DateInput(attrs={'type': 'date'}))
|
||||
employment_type = forms.ChoiceField(label='Beschäftigungsverhältnis', choices=EMPLOYMENT_CHOICES, required=True)
|
||||
|
||||
Reference in New Issue
Block a user