from io import BytesIO from django.contrib import messages from django.contrib.auth import get_user_model, login as auth_login from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext as _ from .branding import get_branding_email_copy from .forms import AccountAvatarForm, AccountDetailsForm, AccountNotificationPreferencesForm, AccountTOTPDisableForm, AccountTOTPEnableForm, AccountTOTPRegenerateRecoveryCodesForm, AppLoginForm, AppTOTPChallengeForm from .models import UserProfile from .roles import get_user_role_label from .totp import build_otpauth_uri, generate_recovery_codes, generate_totp_secret def login_page_impl(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_impl(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, }, ) def account_profile_page_impl(request): session_secret_key = 'account_totp_pending_secret' session_codes_key = 'account_totp_recovery_codes' profile, created = UserProfile.objects.get_or_create(user=request.user) recovery_codes = request.session.pop(session_codes_key, []) pending_totp_secret = request.session.get(session_secret_key) or '' if profile.totp_enabled: pending_totp_secret = '' request.session.pop(session_secret_key, None) elif not pending_totp_secret: pending_totp_secret = generate_totp_secret() request.session[session_secret_key] = pending_totp_secret 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() if form_kind == 'avatar': avatar_form = AccountAvatarForm(request.POST, request.FILES, instance=profile) if avatar_form.is_valid(): avatar_form.save() messages.success(request, _('Profilbild gespeichert.')) return redirect('account_profile_page') messages.error(request, _('Profilbild konnte nicht gespeichert werden.')) elif form_kind == 'details': account_edit_open = True details_form = AccountDetailsForm(request.POST, user=request.user, profile=profile) if details_form.is_valid(): details_form.save() 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) if totp_enable_form.is_valid(): recovery_codes = generate_recovery_codes() profile.enable_totp(pending_totp_secret, recovery_codes) request.session[session_codes_key] = recovery_codes request.session.pop(session_secret_key, None) messages.success(request, _('TOTP wurde aktiviert.')) return redirect('account_profile_page') messages.error(request, _('TOTP konnte nicht aktiviert werden.')) elif form_kind == 'totp_disable': totp_edit_open = True totp_disable_form = AccountTOTPDisableForm(request.POST, user=request.user, profile=profile) if totp_disable_form.is_valid(): profile.disable_totp() request.session.pop(session_secret_key, None) messages.success(request, _('TOTP wurde deaktiviert.')) return redirect('account_profile_page') messages.error(request, _('TOTP konnte nicht deaktiviert werden.')) elif form_kind == 'totp_regenerate_codes': totp_edit_open = True totp_regenerate_form = AccountTOTPRegenerateRecoveryCodesForm(request.POST, user=request.user, profile=profile) if totp_regenerate_form.is_valid(): recovery_codes = generate_recovery_codes() profile.set_recovery_codes(recovery_codes) profile.save(update_fields=['totp_recovery_codes', 'updated_at']) request.session[session_codes_key] = recovery_codes messages.success(request, _('Recovery-Codes wurden neu erzeugt.')) return redirect('account_profile_page') messages.error(request, _('Recovery-Codes konnten nicht neu erzeugt werden.')) branding_context = get_branding_email_copy() totp_account_name = (request.user.email or request.user.username or '').strip() totp_issuer = (branding_context.get('portal_title') or branding_context.get('company_name') or 'Workdock').strip() totp_otpauth_uri = '' if profile.totp_enabled else build_otpauth_uri(pending_totp_secret, account_name=totp_account_name, issuer=totp_issuer) totp_qr_svg = '' if totp_otpauth_uri: try: import qrcode import qrcode.image.svg qr_image = qrcode.make(totp_otpauth_uri, image_factory=qrcode.image.svg.SvgPathImage) stream = BytesIO() qr_image.save(stream) totp_qr_svg = stream.getvalue().decode('utf-8') except Exception: totp_qr_svg = '' return render( request, 'workflows/account_profile.html', { 'account_user': request.user, '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, 'totp_otpauth_uri': totp_otpauth_uri, 'totp_qr_svg': totp_qr_svg, 'totp_recovery_codes': recovery_codes, }, )