feat: Implement TOTP-based Two-Factor Authentication
- Add django-otp and qrcode dependencies - Create comprehensive 2FA views and templates in German - Add 2FA setup, verification, and management interfaces - Implement backup token system with 10 recovery codes - Add TwoFactorMiddleware for session enforcement - Integrate 2FA controls into user navigation menu - Support QR code generation for authenticator apps - Add forms for secure 2FA operations with validation - Configure OTP settings and admin site integration Features: - Optional 2FA (users can enable/disable) - TOTP compatible with Google Authenticator, Authy, etc. - Backup codes for emergency access - German language interface - Session-based 2FA enforcement - Password confirmation for sensitive operations - Production-ready with HTTPS support
This commit is contained in:
@@ -34,6 +34,9 @@ INSTALLED_APPS = [
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.humanize",
|
||||
"rest_framework",
|
||||
"django_otp",
|
||||
"django_otp.plugins.otp_totp",
|
||||
"django_otp.plugins.otp_static",
|
||||
"stiftung",
|
||||
]
|
||||
# Add this to app/core/settings.py
|
||||
@@ -46,6 +49,8 @@ MIDDLEWARE = [
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django_otp.middleware.OTPMiddleware",
|
||||
"stiftung.middleware.TwoFactorMiddleware", # 2FA enforcement middleware
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"stiftung.middleware.AuditMiddleware", # Audit logging middleware
|
||||
@@ -134,3 +139,14 @@ if not DEBUG:
|
||||
SECURE_HSTS_SECONDS = 31536000 # 1 year
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
|
||||
# =============================================================================
|
||||
# TWO-FACTOR AUTHENTICATION SETTINGS
|
||||
# =============================================================================
|
||||
|
||||
# django-otp settings
|
||||
OTP_TOTP_ISSUER = 'Stiftung Management System'
|
||||
OTP_LOGIN_URL = '/two-factor/login/'
|
||||
|
||||
# Optional: Hide sensitive data in admin when not verified
|
||||
OTP_ADMIN_HIDE_SENSITIVE_DATA = True
|
||||
|
||||
@@ -16,6 +16,7 @@ urlpatterns = [
|
||||
name="login",
|
||||
),
|
||||
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
|
||||
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
Vorname;Nachname;E-Mail;Telefon;IBAN;Straße;PLZ;Ort;Personentyp;Geburtsdatum;Pachtnummer;Pachtbeginn_Erste;Pachtende_Letzte;Pachtzins_Aktuell;Landwirtschaftliche_Ausbildung;Berufserfahrung_Jahre;Spezialisierung;Notizen;Aktiv
|
||||
N/A;Groiner Milch KG;;;;Groiner Allee 18;46459;Rees;Gesellschaft;;;;;;;;;;WAHR
|
||||
N/A;M u D Becker GbR;;;;Helweg 76;46348;Raesfeld;Gesellschaft;;;;;;;;;;WAHR
|
||||
Walter;Buchmann;;;;Büskes Heide 11;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
|
||||
Andreas;Elbers;;;;Grietherbusch Nr. 15;46459;Rees;Herrn;;;;;;;;;;WAHR
|
||||
N/A;Fischer GbR;;;;Werricher Strasse 1;46487;Wesel;Gesellschaft;;;;;;;;;;WAHR
|
||||
Michael;Ingenbleek;;;;Achterhoeker Schulweg 39 a;47626;Kevelaer;Herrn;;;;;;;;;;WAHR
|
||||
N/A;Bröcheler KG;;;;Kervenheimer Str. 30;47626;Kevelaer;Gesellschaft;;;;;;;;;;WAHR
|
||||
Jens;Bodden;;;;Moelscher Weg 16;47574;Goch;Herrn;;;;;;;;;;WAHR
|
||||
Marcel;Müller;;;;Weysche Strasse 55;47546;Kalkar-Hanselaer;Herrn;;;;;;;;;;WAHR
|
||||
Ludger;Paeßens;;;;Bergerfurther Strasse 4;46499;Hamminkeln;Herrn;;;;;;;;;;WAHR
|
||||
N/A;Sander GbR;;;;Hardtbergweg 21;46569;Hünxe;Gesellschaft;;;;;;;;;;WAHR
|
||||
Dirk;Schmäh;;;;Schlümersweg 7 A;46499;Hamminkeln-Brünen;Herrn;;;;;;;;;;WAHR
|
||||
Wolfgang;Schmitz;;;;Höst-Vornicker-Weg 1;47652;Weeze;Herrn;;;;;;;;;;WAHR
|
||||
Sebastian;Scholten;;;;Underbergsheide 46;46485;Wesel-Obrighoven;Herrn;;;;;;;;;;WAHR
|
||||
N/A;Ulmenhorst GbR;;;;Schlootweg 10;46499;Hamminkeln;Gesellschaft;;;;;;;;;;WAHR
|
||||
Simone;Gerten;;;;Obrighovener Str. 107;46485;Wesel;Frau;;;;;;;;;;WAHR
|
||||
Günther;Engelmann;;;;Loher Weg 42;46484;Wesel;Herrn;;;;;;;;;;WAHR
|
||||
N/A;Lühlshof KG;;;;Auf dem Heidchen 27;46485;Wesel;Gesellschaft;;;;;;;;;;WAHR
|
||||
Walter;Schmidt;;;;Ilserheider Str. 4;32469;Petershagen;Herrn;;;;;;;;;;WAHR
|
||||
N/A;Inge und Tom Rose GbR;;;;Kohlbreite 22;34414;Warburg-Calenberg;Gesellschaft;;;;;;;;;;WAHR
|
||||
N/A;Stiftung;;;;Schwarzensteiner Weg 75;46569;Hünxe;;;;;;;;;;;WAHR
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 207 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 204 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 207 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 207 KiB |
@@ -9,3 +9,5 @@ requests==2.32.3
|
||||
gunicorn==22.0.0
|
||||
python-dateutil==2.9.0
|
||||
markdown==3.6
|
||||
django-otp==1.2.4
|
||||
qrcode[pil]==7.4.2
|
||||
|
||||
11
app/static.backup.20250929_221458/README.md
Normal file
11
app/static.backup.20250929_221458/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Static Files Directory
|
||||
|
||||
This directory contains static files for the Django application.
|
||||
|
||||
## Structure:
|
||||
- `css/` - Custom CSS files
|
||||
- `js/` - Custom JavaScript files
|
||||
- `img/` - Image assets
|
||||
- `fonts/` - Font files
|
||||
|
||||
Static files are collected to `staticfiles/` directory during deployment via `python manage.py collectstatic`.
|
||||
@@ -4,3 +4,13 @@ from django.apps import AppConfig
|
||||
class StiftungConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "stiftung"
|
||||
|
||||
def ready(self):
|
||||
# Configure admin site with 2FA support
|
||||
try:
|
||||
from django_otp.admin import OTPAdminSite
|
||||
from django.contrib import admin
|
||||
admin.site.__class__ = OTPAdminSite
|
||||
except ImportError:
|
||||
# django-otp not installed
|
||||
pass
|
||||
|
||||
@@ -1476,3 +1476,84 @@ class VierteljahresNachweisForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
# Two-Factor Authentication Forms
|
||||
|
||||
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'
|
||||
)
|
||||
|
||||
@@ -8,7 +8,10 @@ import threading
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.db.models.signals import post_delete, post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import reverse
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
|
||||
from stiftung.audit import get_client_ip, log_action, track_model_changes
|
||||
|
||||
@@ -36,6 +39,40 @@ class AuditMiddleware(MiddlewareMixin):
|
||||
return response
|
||||
|
||||
|
||||
class TwoFactorMiddleware(MiddlewareMixin):
|
||||
"""
|
||||
Middleware that enforces 2FA verification for users with 2FA enabled
|
||||
"""
|
||||
|
||||
def process_request(self, request):
|
||||
"""Check if user needs 2FA verification"""
|
||||
# Skip if user is not authenticated
|
||||
if not request.user.is_authenticated:
|
||||
return None
|
||||
|
||||
# Skip for admin URLs and 2FA URLs themselves
|
||||
if (request.path.startswith('/admin/') or
|
||||
request.path.startswith('/two-factor/') or
|
||||
request.path.startswith('/auth/2fa/') or
|
||||
request.path == '/logout/' or
|
||||
request.path.startswith('/static/') or
|
||||
request.path.startswith('/media/')):
|
||||
return None
|
||||
|
||||
# Check if user has 2FA enabled
|
||||
has_2fa = TOTPDevice.objects.filter(user=request.user, confirmed=True).exists()
|
||||
|
||||
if has_2fa:
|
||||
# Check if user has completed 2FA verification in this session
|
||||
if not request.session.get('2fa_verified', False):
|
||||
# Redirect to 2FA verification page
|
||||
return HttpResponseRedirect(
|
||||
reverse('stiftung:two_factor_verify') + f'?next={request.path}'
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_current_request():
|
||||
"""Get the current request from thread-local storage"""
|
||||
return getattr(_local, "request", None)
|
||||
|
||||
@@ -331,6 +331,12 @@ urlpatterns = [
|
||||
path(
|
||||
"administration/users/<int:pk>/delete/", views.user_delete, name="user_delete"
|
||||
),
|
||||
# Two-Factor Authentication URLs
|
||||
path("auth/2fa/setup/", views.two_factor_setup, name="two_factor_setup"),
|
||||
path("auth/2fa/qr/", views.two_factor_qr, name="two_factor_qr"),
|
||||
path("auth/2fa/verify/", views.two_factor_verify, name="two_factor_verify"),
|
||||
path("auth/2fa/disable/", views.two_factor_disable, name="two_factor_disable"),
|
||||
path("auth/2fa/backup-tokens/", views.backup_tokens, name="backup_tokens"),
|
||||
# Hilfsbox URLs
|
||||
path("help-box/edit/", views.edit_help_box, name="edit_help_box"),
|
||||
path("help-box/admin/", views.edit_help_box, name="help_boxes_admin"),
|
||||
|
||||
@@ -6,6 +6,8 @@ import time
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
@@ -14,10 +16,14 @@ from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import JsonResponse
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
@@ -7804,3 +7810,214 @@ def quarterly_confirmation_reset(request, pk):
|
||||
)
|
||||
|
||||
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
||||
|
||||
|
||||
# Two-Factor Authentication Views
|
||||
|
||||
@login_required
|
||||
def two_factor_setup(request):
|
||||
"""Setup or manage TOTP 2FA for the current user"""
|
||||
|
||||
# Check if user already has TOTP device
|
||||
device = TOTPDevice.objects.filter(user=request.user, confirmed=True).first()
|
||||
static_device = StaticDevice.objects.filter(user=request.user).first()
|
||||
|
||||
if device:
|
||||
# User has 2FA enabled - show management options
|
||||
context = {
|
||||
'has_2fa': True,
|
||||
'device': device,
|
||||
'backup_token_count': static_device.token_set.count() if static_device else 0,
|
||||
'title': 'Zwei-Faktor-Authentifizierung verwalten'
|
||||
}
|
||||
return render(request, 'stiftung/auth/two_factor_manage.html', context)
|
||||
|
||||
# User doesn't have 2FA - show setup
|
||||
# Get or create unconfirmed TOTP device
|
||||
device, created = TOTPDevice.objects.get_or_create(
|
||||
user=request.user,
|
||||
name='default',
|
||||
defaults={'confirmed': False}
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
token = request.POST.get('token', '').strip()
|
||||
if device.verify_token(token):
|
||||
device.confirmed = True
|
||||
device.save()
|
||||
|
||||
# Generate backup tokens
|
||||
static_device = StaticDevice.objects.create(
|
||||
user=request.user,
|
||||
name='backup'
|
||||
)
|
||||
|
||||
backup_tokens = []
|
||||
for _ in range(10): # Generate 10 backup codes
|
||||
token_value = random_hex()[:8] # 8 character backup codes
|
||||
StaticToken.objects.create(
|
||||
device=static_device,
|
||||
token=token_value
|
||||
)
|
||||
backup_tokens.append(token_value)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
"Zwei-Faktor-Authentifizierung wurde erfolgreich aktiviert! "
|
||||
"Bitte speichern Sie Ihre Backup-Codes sicher."
|
||||
)
|
||||
|
||||
return render(request, 'stiftung/auth/backup_tokens.html', {
|
||||
'backup_tokens': backup_tokens,
|
||||
'title': 'Backup-Codes'
|
||||
})
|
||||
else:
|
||||
messages.error(request, "Ungültiger Bestätigungscode. Bitte versuchen Sie es erneut.")
|
||||
|
||||
# Generate QR code URL
|
||||
qr_url = device.config_url
|
||||
|
||||
context = {
|
||||
'device': device,
|
||||
'qr_url': qr_url,
|
||||
'title': 'Zwei-Faktor-Authentifizierung einrichten'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/auth/two_factor_setup.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def two_factor_qr(request):
|
||||
"""Generate QR code for TOTP setup"""
|
||||
device = TOTPDevice.objects.filter(user=request.user, confirmed=False).first()
|
||||
|
||||
if not device:
|
||||
return HttpResponse("Kein Setup-Device gefunden", status=404)
|
||||
|
||||
# Generate QR code
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
qr.add_data(device.config_url)
|
||||
qr.make(fit=True)
|
||||
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
response = HttpResponse(content_type="image/png")
|
||||
img.save(response, "PNG")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
def two_factor_verify(request):
|
||||
"""Verify TOTP token during login process"""
|
||||
if request.method == "POST":
|
||||
token = request.POST.get('otp_token', '').strip()
|
||||
|
||||
# Check TOTP devices
|
||||
devices = TOTPDevice.objects.filter(user=request.user, confirmed=True)
|
||||
for device in devices:
|
||||
if device.verify_token(token):
|
||||
request.session['2fa_verified'] = True
|
||||
messages.success(request, "Zwei-Faktor-Authentifizierung erfolgreich.")
|
||||
return redirect(request.GET.get('next', 'stiftung:dashboard'))
|
||||
|
||||
# Check static backup tokens
|
||||
static_devices = StaticDevice.objects.filter(user=request.user)
|
||||
for device in static_devices:
|
||||
if device.verify_token(token):
|
||||
request.session['2fa_verified'] = True
|
||||
messages.success(request, "Backup-Code erfolgreich verwendet.")
|
||||
return redirect(request.GET.get('next', 'stiftung:dashboard'))
|
||||
|
||||
messages.error(request, "Ungültiger Code. Bitte versuchen Sie es erneut.")
|
||||
|
||||
context = {
|
||||
'title': 'Zwei-Faktor-Authentifizierung',
|
||||
'next': request.GET.get('next', '')
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/auth/two_factor_verify.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def two_factor_disable(request):
|
||||
"""Disable TOTP 2FA for the current user"""
|
||||
if request.method == "POST":
|
||||
password = request.POST.get('password', '')
|
||||
|
||||
if request.user.check_password(password):
|
||||
# Remove all TOTP devices
|
||||
TOTPDevice.objects.filter(user=request.user).delete()
|
||||
|
||||
# Remove all static backup token devices
|
||||
StaticDevice.objects.filter(user=request.user).delete()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
"Zwei-Faktor-Authentifizierung wurde erfolgreich deaktiviert."
|
||||
)
|
||||
return redirect("stiftung:dashboard")
|
||||
else:
|
||||
messages.error(request, "Ungültiges Passwort.")
|
||||
|
||||
context = {
|
||||
'title': 'Zwei-Faktor-Authentifizierung deaktivieren'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/auth/two_factor_disable.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def backup_tokens(request):
|
||||
"""Display or regenerate backup tokens"""
|
||||
static_device = StaticDevice.objects.filter(user=request.user).first()
|
||||
|
||||
if request.method == "POST" and 'regenerate' in request.POST:
|
||||
password = request.POST.get('password', '')
|
||||
|
||||
if request.user.check_password(password):
|
||||
# Delete old tokens
|
||||
if static_device:
|
||||
static_device.delete()
|
||||
|
||||
# Generate new backup tokens
|
||||
static_device = StaticDevice.objects.create(
|
||||
user=request.user,
|
||||
name='backup'
|
||||
)
|
||||
|
||||
backup_tokens = []
|
||||
for _ in range(10): # Generate 10 backup codes
|
||||
token_value = random_hex()[:8] # 8 character backup codes
|
||||
StaticToken.objects.create(
|
||||
device=static_device,
|
||||
token=token_value
|
||||
)
|
||||
backup_tokens.append(token_value)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
"Neue Backup-Codes wurden generiert. Bitte speichern Sie diese sicher."
|
||||
)
|
||||
|
||||
context = {
|
||||
'backup_tokens': backup_tokens,
|
||||
'title': 'Neue Backup-Codes'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/auth/backup_tokens.html', context)
|
||||
else:
|
||||
messages.error(request, "Ungültiges Passwort.")
|
||||
|
||||
# Show existing tokens (count only for security)
|
||||
token_count = 0
|
||||
if static_device:
|
||||
token_count = static_device.token_set.count()
|
||||
|
||||
context = {
|
||||
'token_count': token_count,
|
||||
'has_tokens': token_count > 0,
|
||||
'title': 'Backup-Codes'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/auth/backup_tokens_manage.html', context)
|
||||
|
||||
@@ -622,6 +622,12 @@
|
||||
<li><a class="dropdown-item" href="{% url 'stiftung:user_detail' user.pk %}">
|
||||
<i class="fas fa-user me-2"></i>Mein Profil
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">Sicherheit</h6></li>
|
||||
<li><a class="dropdown-item" href="{% url 'stiftung:two_factor_setup' %}">
|
||||
<i class="fas fa-shield-alt me-2"></i>2FA verwalten
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{% if perms.stiftung.manage_users %}
|
||||
<li><a class="dropdown-item" href="{% url 'stiftung:user_management' %}">
|
||||
<i class="fas fa-users me-2"></i>Benutzerverwaltung
|
||||
|
||||
157
app/templates/stiftung/auth/backup_tokens.html
Normal file
157
app/templates/stiftung/auth/backup_tokens.html
Normal file
@@ -0,0 +1,157 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-key text-success"></i>
|
||||
Backup-Codes für Zwei-Faktor-Authentifizierung
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-success">
|
||||
<h5><i class="fas fa-check-circle"></i> Zwei-Faktor-Authentifizierung aktiviert!</h5>
|
||||
<p class="mb-0">
|
||||
Ihr Konto ist jetzt mit Zwei-Faktor-Authentifizierung geschützt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<h6><i class="fas fa-exclamation-triangle"></i> Wichtig: Backup-Codes sicher speichern</h6>
|
||||
<p class="mb-0">
|
||||
Diese Backup-Codes können Sie verwenden, falls Sie keinen Zugriff auf Ihre
|
||||
Authenticator-App haben. <strong>Speichern Sie diese Codes an einem sicheren Ort!</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Ihre Backup-Codes:</h5>
|
||||
<div class="bg-light p-3 rounded mb-3" id="backup-codes">
|
||||
{% for token in backup_tokens %}
|
||||
<div class="font-monospace fw-bold mb-2">{{ token }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-outline-primary" onclick="copyBackupCodes()">
|
||||
<i class="fas fa-copy"></i>
|
||||
Codes kopieren
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="printBackupCodes()">
|
||||
<i class="fas fa-print"></i>
|
||||
Codes drucken
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>Verwendung der Backup-Codes:</h5>
|
||||
<ul class="text-muted">
|
||||
<li>Jeder Code kann nur <strong>einmal</strong> verwendet werden</li>
|
||||
<li>Geben Sie den Code anstelle des Authenticator-Codes ein</li>
|
||||
<li>Codes sind 8 Zeichen lang (Buchstaben und Zahlen)</li>
|
||||
<li>Bewahren Sie die Codes sicher und vertraulich auf</li>
|
||||
</ul>
|
||||
|
||||
<div class="alert alert-info small">
|
||||
<h6><i class="fas fa-lightbulb"></i> Empfohlene Aufbewahrung:</h6>
|
||||
<ul class="mb-0 small">
|
||||
<li>In einem Passwort-Manager</li>
|
||||
<li>Ausgedruckt in einem Tresor</li>
|
||||
<li>Verschlüsselt auf einem sicheren Gerät</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="{% url 'stiftung:dashboard' %}" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-home"></i>
|
||||
Weiter zum Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyBackupCodes() {
|
||||
const codes = [
|
||||
{% for token in backup_tokens %}'{{ token }}'{% if not forloop.last %},{% endif %}{% endfor %}
|
||||
];
|
||||
const text = codes.join('\n');
|
||||
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
// Show success message
|
||||
const btn = event.target.closest('button');
|
||||
const originalText = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fas fa-check text-success"></i> Kopiert!';
|
||||
btn.classList.add('btn-success');
|
||||
btn.classList.remove('btn-outline-primary');
|
||||
|
||||
setTimeout(function() {
|
||||
btn.innerHTML = originalText;
|
||||
btn.classList.remove('btn-success');
|
||||
btn.classList.add('btn-outline-primary');
|
||||
}, 2000);
|
||||
}).catch(function(err) {
|
||||
alert('Fehler beim Kopieren: ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
function printBackupCodes() {
|
||||
const codes = [
|
||||
{% for token in backup_tokens %}'{{ token }}'{% if not forloop.last %},{% endif %}{% endfor %}
|
||||
];
|
||||
|
||||
const printWindow = window.open('', '_blank');
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Backup-Codes - Stiftung Management System</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; }
|
||||
h1 { color: #333; }
|
||||
.codes { background: #f8f9fa; padding: 20px; border-radius: 5px; }
|
||||
.code { font-family: monospace; font-size: 14px; margin: 10px 0; font-weight: bold; }
|
||||
.warning { color: #d63384; font-weight: bold; margin: 20px 0; }
|
||||
.date { color: #666; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Backup-Codes für Zwei-Faktor-Authentifizierung</h1>
|
||||
<p><strong>Konto:</strong> ${window.location.hostname}</p>
|
||||
<p class="date"><strong>Generiert am:</strong> ${new Date().toLocaleString('de-DE')}</p>
|
||||
|
||||
<div class="warning">
|
||||
⚠️ WICHTIG: Diese Codes sind vertraulich und können nur einmal verwendet werden!
|
||||
</div>
|
||||
|
||||
<div class="codes">
|
||||
${codes.map(code => '<div class="code">' + code + '</div>').join('')}
|
||||
</div>
|
||||
|
||||
<p><strong>Verwendung:</strong></p>
|
||||
<ul>
|
||||
<li>Verwenden Sie diese Codes, falls Sie keinen Zugriff auf Ihre Authenticator-App haben</li>
|
||||
<li>Geben Sie einen Code anstelle des Authenticator-Codes ein</li>
|
||||
<li>Jeder Code kann nur einmal verwendet werden</li>
|
||||
<li>Bewahren Sie diese Codes sicher auf</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
printWindow.print();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
146
app/templates/stiftung/auth/backup_tokens_manage.html
Normal file
146
app/templates/stiftung/auth/backup_tokens_manage.html
Normal file
@@ -0,0 +1,146 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-key text-primary"></i>
|
||||
Backup-Codes verwalten
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if has_tokens %}
|
||||
<div class="alert alert-info">
|
||||
<h6><i class="fas fa-info-circle"></i> Backup-Codes Status</h6>
|
||||
<p class="mb-0">
|
||||
Sie haben derzeit <strong>{{ token_count }} Backup-Codes</strong> verfügbar.
|
||||
Aus Sicherheitsgründen werden die Codes nicht angezeigt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Neue Backup-Codes generieren</h5>
|
||||
<p class="text-muted">
|
||||
Wenn Sie neue Backup-Codes benötigen (z.B. weil Sie alle verbraucht haben
|
||||
oder sie verloren haben), können Sie neue generieren.
|
||||
</p>
|
||||
|
||||
<div class="alert alert-warning small">
|
||||
<strong>Warnung:</strong> Das Generieren neuer Codes macht alle
|
||||
bisherigen Backup-Codes ungültig.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>Passwort eingeben</h5>
|
||||
<p class="text-muted">
|
||||
Geben Sie Ihr Passwort ein, um neue Backup-Codes zu generieren:
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="regenerate" value="1">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Passwort</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
Neue Backup-Codes generieren
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<h6><i class="fas fa-exclamation-triangle"></i> Keine Backup-Codes vorhanden</h6>
|
||||
<p class="mb-0">
|
||||
Sie haben derzeit keine Backup-Codes. Das kann passieren, wenn:
|
||||
</p>
|
||||
<ul class="mb-0 mt-2">
|
||||
<li>Sie alle Codes bereits verwendet haben</li>
|
||||
<li>Die Zwei-Faktor-Authentifizierung neu eingerichtet wurde</li>
|
||||
<li>Die Codes aus anderen Gründen gelöscht wurden</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Neue Backup-Codes generieren</h5>
|
||||
<p class="text-muted">
|
||||
Generieren Sie neue Backup-Codes, um sicherzustellen, dass Sie
|
||||
auch ohne Authenticator-App auf Ihr Konto zugreifen können.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>Passwort eingeben</h5>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="regenerate" value="1">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Passwort</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i>
|
||||
Backup-Codes generieren
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
<h6><i class="fas fa-lightbulb"></i> Über Backup-Codes:</h6>
|
||||
<ul class="mb-0 small">
|
||||
<li><strong>Zweck:</strong> Zugriff auf Ihr Konto, wenn die Authenticator-App nicht verfügbar ist</li>
|
||||
<li><strong>Anzahl:</strong> 10 Codes werden generiert</li>
|
||||
<li><strong>Verwendung:</strong> Jeder Code kann nur einmal verwendet werden</li>
|
||||
<li><strong>Format:</strong> 8 Zeichen (Buchstaben und Zahlen)</li>
|
||||
<li><strong>Sicherheit:</strong> Codes sollten sicher aufbewahrt werden</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="{% url 'stiftung:dashboard' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Zurück zum Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
84
app/templates/stiftung/auth/two_factor_disable.html
Normal file
84
app/templates/stiftung/auth/two_factor_disable.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-shield-alt text-warning"></i>
|
||||
Zwei-Faktor-Authentifizierung deaktivieren
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
<h6><i class="fas fa-exclamation-triangle"></i> Warnung</h6>
|
||||
<p class="mb-0">
|
||||
Sie sind dabei, die Zwei-Faktor-Authentifizierung zu deaktivieren.
|
||||
Dies verringert die Sicherheit Ihres Kontos erheblich.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h5>Was wird deaktiviert:</h5>
|
||||
<ul class="text-muted mb-4">
|
||||
<li>Authenticator-App Codes</li>
|
||||
<li>Alle bestehenden Backup-Codes</li>
|
||||
<li>Zusätzliche Sicherheitsebene beim Login</li>
|
||||
</ul>
|
||||
|
||||
<h5>Bestätigung erforderlich</h5>
|
||||
<p class="text-muted">
|
||||
Geben Sie Ihr Passwort ein, um die Zwei-Faktor-Authentifizierung
|
||||
zu deaktivieren:
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">Passwort</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
autofocus>
|
||||
<div class="form-text">
|
||||
Ihr aktuelles Konto-Passwort
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-warning btn-lg">
|
||||
<i class="fas fa-times-circle"></i>
|
||||
Zwei-Faktor-Authentifizierung deaktivieren
|
||||
</button>
|
||||
<a href="{% url 'stiftung:dashboard' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Abbrechen
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<h6><i class="fas fa-lightbulb"></i> Alternative Empfehlung</h6>
|
||||
<p class="mb-0 small">
|
||||
Statt die 2FA zu deaktivieren, können Sie auch:
|
||||
</p>
|
||||
<ul class="mb-0 small mt-1">
|
||||
<li>Neue Backup-Codes generieren, falls Sie den Zugang verloren haben</li>
|
||||
<li>Die 2FA neu einrichten, falls Probleme mit der App bestehen</li>
|
||||
<li>Den Administrator kontaktieren, wenn Sie Hilfe benötigen</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
124
app/templates/stiftung/auth/two_factor_manage.html
Normal file
124
app/templates/stiftung/auth/two_factor_manage.html
Normal file
@@ -0,0 +1,124 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-shield-alt text-success"></i>
|
||||
Zwei-Faktor-Authentifizierung verwalten
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-success">
|
||||
<h6><i class="fas fa-check-circle"></i> 2FA ist aktiviert</h6>
|
||||
<p class="mb-0">
|
||||
Ihr Konto ist mit Zwei-Faktor-Authentifizierung geschützt.
|
||||
Sie benötigen bei jeder Anmeldung einen Code aus Ihrer Authenticator-App.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-key"></i>
|
||||
Backup-Codes
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small">
|
||||
Backup-Codes ermöglichen den Zugriff auf Ihr Konto,
|
||||
wenn Ihre Authenticator-App nicht verfügbar ist.
|
||||
</p>
|
||||
|
||||
{% if backup_token_count > 0 %}
|
||||
<p class="mb-2">
|
||||
<strong>{{ backup_token_count }} Backup-Codes</strong> verfügbar
|
||||
</p>
|
||||
<div class="d-grid">
|
||||
<a href="{% url 'stiftung:backup_tokens' %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-eye"></i>
|
||||
Codes verwalten
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning small">
|
||||
<strong>Keine Backup-Codes vorhanden!</strong><br>
|
||||
Es wird empfohlen, Backup-Codes zu generieren.
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<a href="{% url 'stiftung:backup_tokens' %}" class="btn btn-warning">
|
||||
<i class="fas fa-plus"></i>
|
||||
Backup-Codes generieren
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-cog"></i>
|
||||
Einstellungen
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small">
|
||||
Verwalten Sie Ihre 2FA-Einstellungen oder
|
||||
deaktivieren Sie die Zwei-Faktor-Authentifizierung.
|
||||
</p>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{% url 'stiftung:two_factor_disable' %}" class="btn btn-outline-danger">
|
||||
<i class="fas fa-times-circle"></i>
|
||||
2FA deaktivieren
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<small class="text-muted">
|
||||
<strong>Gerät eingerichtet:</strong><br>
|
||||
Standard TOTP-Device
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
<h6><i class="fas fa-lightbulb"></i> Tipps für 2FA:</h6>
|
||||
<ul class="mb-0 small">
|
||||
<li><strong>Authenticator-Apps:</strong> Google Authenticator, Microsoft Authenticator, Authy</li>
|
||||
<li><strong>Backup-Codes:</strong> Bewahren Sie diese sicher auf (z.B. Passwort-Manager)</li>
|
||||
<li><strong>Sicherheit:</strong> 2FA schützt auch bei kompromittierten Passwörtern</li>
|
||||
<li><strong>Neues Gerät:</strong> Bei Gerätewechsel 2FA deaktivieren und neu einrichten</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="{% url 'stiftung:dashboard' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Zurück zum Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
129
app/templates/stiftung/auth/two_factor_setup.html
Normal file
129
app/templates/stiftung/auth/two_factor_setup.html
Normal file
@@ -0,0 +1,129 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-shield-alt text-primary"></i>
|
||||
Zwei-Faktor-Authentifizierung einrichten
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Schritt 1: Authenticator App installieren</h5>
|
||||
<p class="text-muted">
|
||||
Installieren Sie eine Authenticator-App auf Ihrem Smartphone:
|
||||
</p>
|
||||
<ul class="mb-4">
|
||||
<li><strong>Google Authenticator</strong> (iOS/Android)</li>
|
||||
<li><strong>Microsoft Authenticator</strong> (iOS/Android)</li>
|
||||
<li><strong>Authy</strong> (iOS/Android/Desktop)</li>
|
||||
<li><strong>1Password</strong> (Premium)</li>
|
||||
</ul>
|
||||
|
||||
<h5>Schritt 2: QR-Code scannen</h5>
|
||||
<p class="text-muted">
|
||||
Scannen Sie den QR-Code mit Ihrer Authenticator-App:
|
||||
</p>
|
||||
<div class="text-center mb-4">
|
||||
<img src="{% url 'stiftung:two_factor_qr' %}"
|
||||
alt="QR Code für 2FA Setup"
|
||||
class="img-fluid border rounded"
|
||||
style="max-width: 200px;">
|
||||
</div>
|
||||
|
||||
<details class="mb-4">
|
||||
<summary class="text-muted small">Manueller Setup-Code anzeigen</summary>
|
||||
<div class="mt-2 p-2 bg-light rounded">
|
||||
<small class="font-monospace">{{ device.key }}</small>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-1">
|
||||
Falls der QR-Code nicht funktioniert, können Sie diesen Code manuell eingeben.
|
||||
</small>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h5>Schritt 3: Bestätigen</h5>
|
||||
<p class="text-muted">
|
||||
Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein:
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="token" class="form-label">Bestätigungscode</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="token"
|
||||
name="token"
|
||||
placeholder="000000"
|
||||
maxlength="6"
|
||||
pattern="[0-9]{6}"
|
||||
required
|
||||
autocomplete="off">
|
||||
<div class="form-text">
|
||||
Der Code wechselt alle 30 Sekunden.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-check"></i>
|
||||
Zwei-Faktor-Authentifizierung aktivieren
|
||||
</button>
|
||||
<a href="{% url 'stiftung:dashboard' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times"></i>
|
||||
Abbrechen
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
<h6><i class="fas fa-info-circle"></i> Wichtige Hinweise:</h6>
|
||||
<ul class="mb-0 small">
|
||||
<li>Nach der Aktivierung erhalten Sie Backup-Codes für den Notfall</li>
|
||||
<li>Bewahren Sie diese Backup-Codes sicher auf</li>
|
||||
<li>Sie benötigen bei jeder Anmeldung einen Code aus der App</li>
|
||||
<li>Die Zwei-Faktor-Authentifizierung erhöht die Sicherheit Ihres Kontos erheblich</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Auto-focus on token input
|
||||
const tokenInput = document.getElementById('token');
|
||||
if (tokenInput) {
|
||||
tokenInput.focus();
|
||||
|
||||
// Auto-submit when 6 digits entered
|
||||
tokenInput.addEventListener('input', function() {
|
||||
if (this.value.length === 6 && /^\d{6}$/.test(this.value)) {
|
||||
// Small delay to allow user to see complete input
|
||||
setTimeout(() => {
|
||||
this.closest('form').submit();
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
89
app/templates/stiftung/auth/two_factor_verify.html
Normal file
89
app/templates/stiftung/auth/two_factor_verify.html
Normal file
@@ -0,0 +1,89 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-shield-alt text-primary"></i>
|
||||
Zwei-Faktor-Authentifizierung
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-center text-muted mb-4">
|
||||
Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein
|
||||
oder verwenden Sie einen Backup-Code.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% if next %}
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="otp_token" class="form-label">Authentifizierungscode</label>
|
||||
<input type="text"
|
||||
class="form-control form-control-lg text-center"
|
||||
id="otp_token"
|
||||
name="otp_token"
|
||||
placeholder="000000"
|
||||
maxlength="8"
|
||||
required
|
||||
autocomplete="off"
|
||||
autofocus>
|
||||
<div class="form-text text-center">
|
||||
6-stelliger Code aus der App oder 8-stelliger Backup-Code
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-sign-in-alt"></i>
|
||||
Bestätigen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<details>
|
||||
<summary class="text-muted small">Probleme beim Anmelden?</summary>
|
||||
<div class="mt-2 small text-muted">
|
||||
<p>Falls Sie keinen Zugriff auf Ihre Authenticator-App haben:</p>
|
||||
<ul class="text-start">
|
||||
<li>Verwenden Sie einen der 8-stelligen Backup-Codes</li>
|
||||
<li>Kontaktieren Sie den Administrator</li>
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tokenInput = document.getElementById('otp_token');
|
||||
if (tokenInput) {
|
||||
// Auto-submit when 6 digits entered (TOTP) or 8 characters (backup code)
|
||||
tokenInput.addEventListener('input', function() {
|
||||
const value = this.value.trim();
|
||||
if ((value.length === 6 && /^\d{6}$/.test(value)) ||
|
||||
(value.length === 8 && /^[a-f0-9]{8}$/i.test(value))) {
|
||||
// Small delay to allow user to see complete input
|
||||
setTimeout(() => {
|
||||
this.closest('form').submit();
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user