snapshot: preserve role-aware notification preferences and operational alerts
This commit is contained in:
@@ -12,7 +12,7 @@ from django.db import IntegrityError
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth import get_user_model, login as auth_login
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.http import JsonResponse
|
||||
@@ -34,7 +34,7 @@ from .backup_ops import (
|
||||
verify_backup_bundle,
|
||||
)
|
||||
from .branding import get_branding_email_copy, get_company_email_domain, get_default_notification_templates, get_portal_trial_config, is_trial_expired
|
||||
from .forms import AccountAvatarForm, AccountDetailsForm, AccountTOTPDisableForm, AccountTOTPEnableForm, AccountTOTPRegenerateRecoveryCodesForm, OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm
|
||||
from .forms import AccountAvatarForm, AccountDetailsForm, AccountNotificationPreferencesForm, AccountTOTPDisableForm, AccountTOTPEnableForm, AccountTOTPRegenerateRecoveryCodesForm, AppLoginForm, AppTOTPChallengeForm, OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm
|
||||
from .form_builder import (
|
||||
DEFAULT_FIELD_ORDER,
|
||||
LOCKED_FIELD_RULES,
|
||||
@@ -43,8 +43,9 @@ from .form_builder import (
|
||||
ONBOARDING_PAGE_ORDER,
|
||||
ensure_form_field_configs,
|
||||
)
|
||||
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserProfile, WorkflowConfig
|
||||
from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, UserNotification, UserProfile, WorkflowConfig
|
||||
from .emailing import send_system_email
|
||||
from .notifications import notify_user
|
||||
from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, assign_user_role, get_user_role_key, get_user_role_label, user_has_capability
|
||||
from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud
|
||||
from .totp import build_otpauth_uri, generate_recovery_codes, generate_totp_secret
|
||||
@@ -67,6 +68,21 @@ def _redirect_back(request, fallback: str):
|
||||
return redirect(referer)
|
||||
return redirect(fallback)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def mark_notification_read(request, notification_id: int):
|
||||
notification = get_object_or_404(UserNotification, id=notification_id, user=request.user)
|
||||
notification.mark_read()
|
||||
return _redirect_back(request, 'home')
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def mark_all_notifications_read(request):
|
||||
UserNotification.objects.filter(user=request.user, read_at__isnull=True).update(read_at=timezone.now())
|
||||
return _redirect_back(request, 'home')
|
||||
|
||||
ONBOARDING_GROUPS = {
|
||||
'business-card-box': ['business_card_name', 'business_card_title', 'business_card_email', 'business_card_phone'],
|
||||
'employment-end-box': ['employment_end_date'],
|
||||
@@ -128,6 +144,85 @@ def healthz(request):
|
||||
)
|
||||
|
||||
|
||||
def login_page(request):
|
||||
if getattr(request.user, 'is_authenticated', False):
|
||||
return redirect('home')
|
||||
|
||||
next_target = (request.POST.get('next') or request.GET.get('next') or '').strip()
|
||||
form = AppLoginForm(request=request, data=request.POST or None)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
user = form.get_user()
|
||||
profile, _ = UserProfile.objects.get_or_create(user=user)
|
||||
safe_next = next_target if next_target.startswith('/') else ''
|
||||
if profile.totp_enabled:
|
||||
request.session['login_pending_user_id'] = user.pk
|
||||
request.session['login_pending_backend'] = getattr(user, 'backend', '')
|
||||
request.session['login_pending_next'] = safe_next
|
||||
return redirect('login_totp')
|
||||
|
||||
auth_login(request, user, backend=getattr(user, 'backend', None))
|
||||
now_ts = int(timezone.now().timestamp())
|
||||
request.session['auth_fresh_ts'] = now_ts
|
||||
request.session['last_activity_ts'] = now_ts
|
||||
return redirect(safe_next or reverse('home'))
|
||||
|
||||
request.session.pop('login_pending_user_id', None)
|
||||
request.session.pop('login_pending_backend', None)
|
||||
request.session.pop('login_pending_next', None)
|
||||
return render(
|
||||
request,
|
||||
'workflows/auth/login.html',
|
||||
{
|
||||
'form': form,
|
||||
'next': next_target,
|
||||
'login_step': 'password',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def login_totp_page(request):
|
||||
if getattr(request.user, 'is_authenticated', False):
|
||||
return redirect('home')
|
||||
|
||||
pending_user_id = request.session.get('login_pending_user_id')
|
||||
backend_path = (request.session.get('login_pending_backend') or '').strip()
|
||||
next_target = (request.session.get('login_pending_next') or '').strip()
|
||||
if not pending_user_id:
|
||||
return redirect('login')
|
||||
|
||||
user = get_object_or_404(get_user_model(), pk=pending_user_id)
|
||||
profile, _ = UserProfile.objects.get_or_create(user=user)
|
||||
if not profile.totp_enabled:
|
||||
request.session.pop('login_pending_user_id', None)
|
||||
request.session.pop('login_pending_backend', None)
|
||||
request.session.pop('login_pending_next', None)
|
||||
return redirect('login')
|
||||
|
||||
show_recovery = request.method == 'POST' and bool((request.POST.get('recovery_code') or '').strip())
|
||||
form = AppTOTPChallengeForm(data=request.POST or None, profile=profile)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
auth_login(request, user, backend=backend_path or 'django.contrib.auth.backends.ModelBackend')
|
||||
request.session.pop('login_pending_user_id', None)
|
||||
request.session.pop('login_pending_backend', None)
|
||||
request.session.pop('login_pending_next', None)
|
||||
now_ts = int(timezone.now().timestamp())
|
||||
request.session['auth_fresh_ts'] = now_ts
|
||||
request.session['last_activity_ts'] = now_ts
|
||||
return redirect(next_target if next_target.startswith('/') else reverse('home'))
|
||||
|
||||
return render(
|
||||
request,
|
||||
'workflows/auth/login.html',
|
||||
{
|
||||
'form': form,
|
||||
'next': next_target,
|
||||
'login_step': 'totp',
|
||||
'login_totp_user': user,
|
||||
'show_recovery_code': show_recovery,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def account_profile_page(request):
|
||||
session_secret_key = 'account_totp_pending_secret'
|
||||
@@ -144,10 +239,12 @@ def account_profile_page(request):
|
||||
|
||||
avatar_form = AccountAvatarForm(instance=profile)
|
||||
details_form = AccountDetailsForm(user=request.user, profile=profile)
|
||||
notification_preferences_form = AccountNotificationPreferencesForm(profile=profile, user=request.user)
|
||||
totp_enable_form = AccountTOTPEnableForm(user=request.user, secret=pending_totp_secret)
|
||||
totp_disable_form = AccountTOTPDisableForm(user=request.user, profile=profile)
|
||||
totp_regenerate_form = AccountTOTPRegenerateRecoveryCodesForm(user=request.user, profile=profile)
|
||||
account_edit_open = False
|
||||
notifications_edit_open = False
|
||||
totp_edit_open = False
|
||||
if request.method == 'POST':
|
||||
form_kind = (request.POST.get('account_form') or '').strip()
|
||||
@@ -166,6 +263,14 @@ def account_profile_page(request):
|
||||
messages.success(request, _('Profildaten gespeichert.'))
|
||||
return redirect('account_profile_page')
|
||||
messages.error(request, _('Profildaten konnten nicht gespeichert werden.'))
|
||||
elif form_kind == 'notification_preferences':
|
||||
notifications_edit_open = True
|
||||
notification_preferences_form = AccountNotificationPreferencesForm(request.POST, profile=profile, user=request.user)
|
||||
if notification_preferences_form.is_valid():
|
||||
notification_preferences_form.save()
|
||||
messages.success(request, _('Benachrichtigungseinstellungen gespeichert.'))
|
||||
return redirect('account_profile_page')
|
||||
messages.error(request, _('Benachrichtigungseinstellungen konnten nicht gespeichert werden.'))
|
||||
elif form_kind == 'totp_enable':
|
||||
totp_edit_open = True
|
||||
totp_enable_form = AccountTOTPEnableForm(request.POST, user=request.user, secret=pending_totp_secret)
|
||||
@@ -222,10 +327,13 @@ def account_profile_page(request):
|
||||
'account_user_profile': profile,
|
||||
'avatar_form': avatar_form,
|
||||
'details_form': details_form,
|
||||
'notification_preferences_form': notification_preferences_form,
|
||||
'notification_preference_groups': notification_preferences_form.grouped_fields(),
|
||||
'totp_enable_form': totp_enable_form,
|
||||
'totp_disable_form': totp_disable_form,
|
||||
'totp_regenerate_form': totp_regenerate_form,
|
||||
'account_edit_open': account_edit_open,
|
||||
'notifications_edit_open': notifications_edit_open,
|
||||
'totp_edit_open': totp_edit_open,
|
||||
'role_label': get_user_role_label(request.user),
|
||||
'totp_pending_secret': pending_totp_secret,
|
||||
@@ -757,7 +865,7 @@ def _build_branding_sections(form, branding):
|
||||
'fields': ['portal_title', 'company_name', 'company_domain', 'default_language', 'login_subtitle'],
|
||||
'field_full': {'login_subtitle'},
|
||||
'hint_map': {
|
||||
'company_domain': _('Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. B. tub.co.'),
|
||||
'company_domain': _('Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. B. workdock.de.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -973,6 +1081,35 @@ def save_portal_trial_config(request):
|
||||
'auto_cleanup_enabled': trial_config.auto_cleanup_enabled,
|
||||
},
|
||||
)
|
||||
if trial_config.is_trial_mode and trial_config.trial_expires_at:
|
||||
remaining = trial_config.trial_expires_at - timezone.now()
|
||||
if remaining.total_seconds() <= 0:
|
||||
notify_user(
|
||||
user=request.user,
|
||||
title=_('Trial ist abgelaufen'),
|
||||
body=_('Der Trial-Zeitraum ist überschritten. Nicht-Platform-Owner werden jetzt blockiert.'),
|
||||
level=UserNotification.LEVEL_WARNING,
|
||||
link_url='/admin-tools/trial/',
|
||||
event_key=UserProfile.NOTIFICATION_TRIAL_ALERTS,
|
||||
)
|
||||
elif remaining <= timedelta(days=7):
|
||||
notify_user(
|
||||
user=request.user,
|
||||
title=_('Trial läuft bald ab'),
|
||||
body=_('Der Trial endet am %(date)s.') % {'date': timezone.localtime(trial_config.trial_expires_at).strftime('%d.%m.%Y %H:%M')},
|
||||
level=UserNotification.LEVEL_WARNING,
|
||||
link_url='/admin-tools/trial/',
|
||||
event_key=UserProfile.NOTIFICATION_TRIAL_ALERTS,
|
||||
)
|
||||
elif not trial_config.is_trial_mode:
|
||||
notify_user(
|
||||
user=request.user,
|
||||
title=_('Trial-Modus deaktiviert'),
|
||||
body=_('Der Trial-Modus wurde ausgeschaltet.'),
|
||||
level=UserNotification.LEVEL_INFO,
|
||||
link_url='/admin-tools/trial/',
|
||||
event_key=UserProfile.NOTIFICATION_TRIAL_ALERTS,
|
||||
)
|
||||
messages.success(request, _('Trial-Konfiguration wurde gespeichert.'))
|
||||
return render(
|
||||
request,
|
||||
@@ -1196,8 +1333,24 @@ def create_backup_from_admin(request):
|
||||
target_label=result['name'],
|
||||
details={'path': result['path']},
|
||||
)
|
||||
notify_user(
|
||||
user=request.user,
|
||||
title=_('Backup erstellt: %(name)s') % {'name': result['name']},
|
||||
body=_('Das Backup-Bundle wurde erfolgreich erstellt.'),
|
||||
level=UserNotification.LEVEL_SUCCESS,
|
||||
link_url='/admin-tools/backups/',
|
||||
event_key=UserProfile.NOTIFICATION_BACKUP_SUCCESS,
|
||||
)
|
||||
messages.success(request, _('Backup wurde erstellt: %(name)s') % {'name': result['name']})
|
||||
except Exception as exc:
|
||||
notify_user(
|
||||
user=request.user,
|
||||
title=_('Backup fehlgeschlagen'),
|
||||
body=str(exc),
|
||||
level=UserNotification.LEVEL_ERROR,
|
||||
link_url='/admin-tools/backups/',
|
||||
event_key=UserProfile.NOTIFICATION_BACKUP_FAILURE,
|
||||
)
|
||||
messages.error(request, _('Backup konnte nicht erstellt werden: %(error)s') % {'error': exc})
|
||||
return redirect('backup_recovery_page')
|
||||
|
||||
@@ -1214,8 +1367,24 @@ def verify_backup_from_admin(request, backup_name: str):
|
||||
target_label=backup_name,
|
||||
details={'summary': result['summary']},
|
||||
)
|
||||
notify_user(
|
||||
user=request.user,
|
||||
title=_('Backup verifiziert: %(name)s') % {'name': result['name']},
|
||||
body=result.get('summary') or _('Das Backup wurde erfolgreich verifiziert.'),
|
||||
level=UserNotification.LEVEL_SUCCESS,
|
||||
link_url='/admin-tools/backups/',
|
||||
event_key=UserProfile.NOTIFICATION_BACKUP_SUCCESS,
|
||||
)
|
||||
messages.success(request, _('Backup wurde verifiziert: %(name)s') % {'name': result['name']})
|
||||
except Exception as exc:
|
||||
notify_user(
|
||||
user=request.user,
|
||||
title=_('Backup-Verifikation fehlgeschlagen'),
|
||||
body=str(exc),
|
||||
level=UserNotification.LEVEL_ERROR,
|
||||
link_url='/admin-tools/backups/',
|
||||
event_key=UserProfile.NOTIFICATION_BACKUP_FAILURE,
|
||||
)
|
||||
messages.error(request, _('Backup-Verifikation fehlgeschlagen: %(error)s') % {'error': exc})
|
||||
return redirect('backup_recovery_page')
|
||||
|
||||
@@ -2492,17 +2661,36 @@ def form_builder_save_order(request):
|
||||
def send_test_email(request):
|
||||
mode = 'TEST_MODE_ON' if is_email_test_mode() else 'TEST_MODE_OFF'
|
||||
redirect_email = get_email_test_redirect()
|
||||
send_system_email(
|
||||
subject=f'SMTP test from onboarding/offboarding v2 ({mode})',
|
||||
body=(
|
||||
'This is a test email. If you see this, SMTP is configured correctly.\n'
|
||||
f'EMAIL_TEST_MODE={is_email_test_mode()}\n'
|
||||
f'EMAIL_TEST_REDIRECT={redirect_email}\n'
|
||||
),
|
||||
to=[settings.TEST_NOTIFICATION_EMAIL],
|
||||
)
|
||||
_audit(request, 'smtp_test_sent', target_type='system_email', target_label=settings.TEST_NOTIFICATION_EMAIL, details={'email_test_mode': is_email_test_mode()})
|
||||
messages.success(request, f'SMTP-Testmail wurde gesendet ({mode}).')
|
||||
try:
|
||||
send_system_email(
|
||||
subject=f'SMTP test from onboarding/offboarding v2 ({mode})',
|
||||
body=(
|
||||
'This is a test email. If you see this, SMTP is configured correctly.\n'
|
||||
f'EMAIL_TEST_MODE={is_email_test_mode()}\n'
|
||||
f'EMAIL_TEST_REDIRECT={redirect_email}\n'
|
||||
),
|
||||
to=[settings.TEST_NOTIFICATION_EMAIL],
|
||||
)
|
||||
_audit(request, 'smtp_test_sent', target_type='system_email', target_label=settings.TEST_NOTIFICATION_EMAIL, details={'email_test_mode': is_email_test_mode()})
|
||||
notify_user(
|
||||
user=request.user,
|
||||
title=_('SMTP-Test erfolgreich'),
|
||||
body=_('Die SMTP-Testmail wurde erfolgreich gesendet.'),
|
||||
level=UserNotification.LEVEL_SUCCESS,
|
||||
link_url='/admin-tools/integrations/',
|
||||
event_key=UserProfile.NOTIFICATION_SYSTEM_ALERTS,
|
||||
)
|
||||
messages.success(request, f'SMTP-Testmail wurde gesendet ({mode}).')
|
||||
except Exception as exc:
|
||||
notify_user(
|
||||
user=request.user,
|
||||
title=_('SMTP-Test fehlgeschlagen'),
|
||||
body=str(exc),
|
||||
level=UserNotification.LEVEL_ERROR,
|
||||
link_url='/admin-tools/integrations/',
|
||||
event_key=UserProfile.NOTIFICATION_SYSTEM_ALERTS,
|
||||
)
|
||||
messages.error(request, _('SMTP-Testmail konnte nicht gesendet werden: %(error)s') % {'error': exc})
|
||||
return _redirect_back(request, 'home')
|
||||
|
||||
|
||||
@@ -2525,11 +2713,35 @@ def nextcloud_test_upload(request):
|
||||
ok = upload_to_nextcloud(temp_path, filename)
|
||||
if ok:
|
||||
_audit(request, 'nextcloud_test_upload', target_type='nextcloud', target_label=filename, details={'result': 'success'})
|
||||
notify_user(
|
||||
user=request.user,
|
||||
title=_('Nextcloud-Test erfolgreich'),
|
||||
body=_('Der Testupload nach Nextcloud war erfolgreich.'),
|
||||
level=UserNotification.LEVEL_SUCCESS,
|
||||
link_url='/admin-tools/integrations/',
|
||||
event_key=UserProfile.NOTIFICATION_SYSTEM_ALERTS,
|
||||
)
|
||||
messages.success(request, f'Nextcloud-Testupload erfolgreich: {filename}')
|
||||
else:
|
||||
_audit(request, 'nextcloud_test_upload', target_type='nextcloud', target_label=filename, details={'result': 'error'})
|
||||
notify_user(
|
||||
user=request.user,
|
||||
title=_('Nextcloud-Test fehlgeschlagen'),
|
||||
body=_('Der Testupload nach Nextcloud ist fehlgeschlagen.'),
|
||||
level=UserNotification.LEVEL_ERROR,
|
||||
link_url='/admin-tools/integrations/',
|
||||
event_key=UserProfile.NOTIFICATION_SYSTEM_ALERTS,
|
||||
)
|
||||
messages.error(request, 'Nextcloud-Testupload fehlgeschlagen. Bitte Konfiguration prüfen.')
|
||||
except Exception as exc:
|
||||
notify_user(
|
||||
user=request.user,
|
||||
title=_('Nextcloud-Test fehlgeschlagen'),
|
||||
body=str(exc),
|
||||
level=UserNotification.LEVEL_ERROR,
|
||||
link_url='/admin-tools/integrations/',
|
||||
event_key=UserProfile.NOTIFICATION_SYSTEM_ALERTS,
|
||||
)
|
||||
messages.error(request, f'Nextcloud-Testupload fehlgeschlagen: {exc}')
|
||||
finally:
|
||||
if temp_path and temp_path.exists():
|
||||
|
||||
Reference in New Issue
Block a user