diff --git a/backend/apps/users/__pycache__/admin.cpython-314.pyc b/backend/apps/users/__pycache__/admin.cpython-314.pyc
index 9daed99..0ddf4d2 100644
Binary files a/backend/apps/users/__pycache__/admin.cpython-314.pyc and b/backend/apps/users/__pycache__/admin.cpython-314.pyc differ
diff --git a/backend/apps/users/__pycache__/models.cpython-314.pyc b/backend/apps/users/__pycache__/models.cpython-314.pyc
index f9c8960..13e6b29 100644
Binary files a/backend/apps/users/__pycache__/models.cpython-314.pyc and b/backend/apps/users/__pycache__/models.cpython-314.pyc differ
diff --git a/backend/apps/users/__pycache__/urls.cpython-314.pyc b/backend/apps/users/__pycache__/urls.cpython-314.pyc
index 170fa35..804311d 100644
Binary files a/backend/apps/users/__pycache__/urls.cpython-314.pyc and b/backend/apps/users/__pycache__/urls.cpython-314.pyc differ
diff --git a/backend/apps/users/__pycache__/views.cpython-314.pyc b/backend/apps/users/__pycache__/views.cpython-314.pyc
index 3a6675c..963bdc7 100644
Binary files a/backend/apps/users/__pycache__/views.cpython-314.pyc and b/backend/apps/users/__pycache__/views.cpython-314.pyc differ
diff --git a/backend/apps/users/admin.py b/backend/apps/users/admin.py
index bf5842e..1c05f3f 100644
--- a/backend/apps/users/admin.py
+++ b/backend/apps/users/admin.py
@@ -1,6 +1,15 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
-from .models import User, RegistrationDomainRule, RegistrationRestrictionSetting
+from django.contrib.auth.models import Group
+from .models import User, RegistrationDomainRule, RegistrationRestrictionSetting, UserActivity
+
+SESSION_MONITOR_GROUP = "Session Monitor"
+
+# Ensure "Session Monitor" group exists in Django Admin
+try:
+ Group.objects.get_or_create(name=SESSION_MONITOR_GROUP)
+except Exception:
+ pass
@admin.register(User)
class UserAdmin(BaseUserAdmin):
@@ -18,3 +27,9 @@ class RegistrationDomainRuleAdmin(admin.ModelAdmin):
@admin.register(RegistrationRestrictionSetting)
class RegistrationRestrictionSettingAdmin(admin.ModelAdmin):
list_display = ['is_restriction_enabled']
+
+@admin.register(UserActivity)
+class UserActivityAdmin(admin.ModelAdmin):
+ list_display = ['user', 'guest_name', 'ip_address', 'last_path', 'last_seen']
+ list_filter = ['last_seen']
+ search_fields = ['ip_address', 'guest_name', 'user__username', 'user__email', 'user__display_name', 'last_path']
diff --git a/backend/apps/users/middleware.py b/backend/apps/users/middleware.py
new file mode 100644
index 0000000..7309453
--- /dev/null
+++ b/backend/apps/users/middleware.py
@@ -0,0 +1,44 @@
+from .models import UserActivity
+
+class UserActivityMiddleware:
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ def __call__(self, request):
+ response = self.get_response(request)
+
+ # Record activity for non-static requests
+ path = request.path
+ if not path.startswith('/static/') and not path.startswith('/media/'):
+ try:
+ # Ensure session exists
+ if not request.session.session_key:
+ request.session.create()
+ session_key = request.session.session_key
+
+ # Extract Client IP handling reverse proxies (Traefik / Nginx)
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
+ if x_forwarded_for:
+ ip = x_forwarded_for.split(',')[0].strip()
+ else:
+ ip = request.META.get('REMOTE_ADDR', '')
+
+ user_agent = request.META.get('HTTP_USER_AGENT', '')[:245]
+ user = request.user if request.user.is_authenticated else None
+ guest_name = request.session.get('guest_name', '') or request.COOKIES.get('guest_display_name', '')
+
+ if session_key:
+ UserActivity.objects.update_or_create(
+ session_key=session_key,
+ defaults={
+ 'user': user,
+ 'guest_name': guest_name if not user else '',
+ 'ip_address': ip,
+ 'user_agent': user_agent,
+ 'last_path': path[:245],
+ }
+ )
+ except Exception:
+ pass
+
+ return response
diff --git a/backend/apps/users/migrations/0007_useractivity.py b/backend/apps/users/migrations/0007_useractivity.py
new file mode 100644
index 0000000..4db8a79
--- /dev/null
+++ b/backend/apps/users/migrations/0007_useractivity.py
@@ -0,0 +1,33 @@
+# Generated by Django 5.2.16 on 2026-08-01 10:25
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0006_registrationrestrictionsetting_require_email_verification'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserActivity',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('guest_name', models.CharField(blank=True, max_length=150, null=True)),
+ ('session_key', models.CharField(db_index=True, max_length=255)),
+ ('ip_address', models.CharField(blank=True, default='', max_length=50)),
+ ('user_agent', models.CharField(blank=True, default='', max_length=255)),
+ ('last_path', models.CharField(blank=True, default='', max_length=255)),
+ ('last_seen', models.DateTimeField(auto_now=True)),
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='activities', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'Benutzer-Aktivität / Sitzung',
+ 'verbose_name_plural': 'Benutzer-Aktivitäten / Sitzungen',
+ 'ordering': ['-last_seen'],
+ },
+ ),
+ ]
diff --git a/backend/apps/users/models.py b/backend/apps/users/models.py
index 0d2b5f3..de31b95 100644
--- a/backend/apps/users/models.py
+++ b/backend/apps/users/models.py
@@ -86,3 +86,21 @@ class GuestClaimToken(models.Model):
def __str__(self):
return f"Claim Token for {self.guest_name} ({self.token})"
+
+class UserActivity(models.Model):
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='activities')
+ guest_name = models.CharField(max_length=150, null=True, blank=True)
+ session_key = models.CharField(max_length=255, db_index=True)
+ ip_address = models.CharField(max_length=50, blank=True, default='')
+ user_agent = models.CharField(max_length=255, blank=True, default='')
+ last_path = models.CharField(max_length=255, blank=True, default='')
+ last_seen = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ ordering = ['-last_seen']
+ verbose_name = 'Benutzer-Aktivität / Sitzung'
+ verbose_name_plural = 'Benutzer-Aktivitäten / Sitzungen'
+
+ def __str__(self):
+ name = self.user.get_display_name() if self.user else (self.guest_name or "Gast")
+ return f"{name} ({self.ip_address}) - {self.last_seen}"
diff --git a/backend/apps/users/urls.py b/backend/apps/users/urls.py
index 441cb6e..e6ff52f 100644
--- a/backend/apps/users/urls.py
+++ b/backend/apps/users/urls.py
@@ -5,7 +5,7 @@ from .views import (
RegistrationDomainRuleViewSet, RegistrationRestrictionSettingView,
PendingUsersView, ApproveUserView, AdminUserViewSet,
GenerateClaimLinkView, ClaimInfoView,
- SMTPSettingView, TestSMTPEmailView, UserListView
+ SMTPSettingView, TestSMTPEmailView, UserListView, ActiveSessionsView
)
router = DefaultRouter()
@@ -18,6 +18,7 @@ urlpatterns = [
path('login/', LoginView.as_view(), name='login'),
path('me/', MeView.as_view(), name='me'),
path('all/', UserListView.as_view(), name='all-users'),
+ path('active-sessions/', ActiveSessionsView.as_view(), name='active-sessions'),
path('restriction-setting/', RegistrationRestrictionSettingView.as_view(), name='restriction-setting'),
path('smtp-setting/', SMTPSettingView.as_view(), name='smtp-setting'),
path('smtp-setting/test/', TestSMTPEmailView.as_view(), name='smtp-setting-test'),
diff --git a/backend/apps/users/views.py b/backend/apps/users/views.py
index ee7064c..b78f9e0 100644
--- a/backend/apps/users/views.py
+++ b/backend/apps/users/views.py
@@ -1,10 +1,14 @@
from rest_framework import views, viewsets, permissions, status
+from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from django.contrib.auth import authenticate, get_user_model
from django.shortcuts import get_object_or_404
from django.core.mail import get_connection, EmailMultiAlternatives
-from .models import RegistrationDomainRule, RegistrationRestrictionSetting, GuestClaimToken, EmailVerificationToken, SMTPSetting
+from .models import (
+ RegistrationDomainRule, RegistrationRestrictionSetting, GuestClaimToken,
+ EmailVerificationToken, SMTPSetting, UserActivity
+)
from .serializers import (
UserSerializer, AdminUserSerializer, RegisterSerializer,
RegistrationDomainRuleSerializer, RegistrationRestrictionSettingSerializer, SMTPSettingSerializer
@@ -80,6 +84,17 @@ class AdminUserViewSet(viewsets.ModelViewSet):
user_to_delete.delete()
return Response({'message': f'Benutzer "{name}" wurde erfolgreich gelöscht.'})
+ @action(detail=True, methods=['post'], url_path='confirm-email')
+ def confirm_email(self, request, pk=None):
+ target_user = self.get_object()
+ target_user.is_email_verified = True
+ target_user.is_approved = True
+ target_user.save()
+ EmailVerificationToken.objects.filter(user=target_user).update(is_used=True)
+ return Response({
+ 'message': f'E-Mail-Adresse von {target_user.get_display_name()} ({target_user.email}) wurde manuell von dir als bestätigt markiert.'
+ })
+
class RegisterView(views.APIView):
permission_classes = [permissions.AllowAny]
@@ -406,3 +421,54 @@ class UserListView(views.APIView):
def get(self, request):
users = User.objects.filter(is_active=True).order_by('username')
return Response(UserSerializer(users, many=True).data)
+
+SESSION_MONITOR_GROUP = "Session Monitor"
+
+class IsSessionMonitorGroupOrSuperuser(permissions.BasePermission):
+ def has_permission(self, request, view):
+ if not request.user or not request.user.is_authenticated:
+ return False
+ if request.user.is_superuser:
+ return True
+ return request.user.groups.filter(name=SESSION_MONITOR_GROUP).exists()
+
+class ActiveSessionsView(views.APIView):
+ permission_classes = [IsSessionMonitorGroupOrSuperuser]
+
+ def get(self, request):
+ from datetime import timedelta
+ from django.utils import timezone
+ cutoff = timezone.now() - timedelta(hours=24)
+
+ activities = UserActivity.objects.filter(last_seen__gte=cutoff).select_related('user').order_by('-last_seen')
+
+ data = []
+ for act in activities:
+ user_info = None
+ if act.user:
+ user_info = {
+ 'id': act.user.id,
+ 'username': act.user.username,
+ 'email': act.user.email,
+ 'display_name': act.user.get_display_name(),
+ 'is_admin': act.user.is_admin_user or act.user.is_staff or act.user.is_superuser,
+ }
+
+ data.append({
+ 'id': act.id,
+ 'user': user_info,
+ 'guest_name': act.guest_name if not act.user else None,
+ 'display_name': act.user.get_display_name() if act.user else (act.guest_name or "Unangemeldeter Gast"),
+ 'session_key': act.session_key[:12] + '...',
+ 'ip_address': act.ip_address or '127.0.0.1',
+ 'user_agent': act.user_agent,
+ 'last_path': act.last_path,
+ 'last_seen': act.last_seen.isoformat(),
+ })
+
+ return Response({
+ 'group_name': SESSION_MONITOR_GROUP,
+ 'is_authorized': True,
+ 'count': len(data),
+ 'sessions': data
+ })
diff --git a/backend/db.sqlite3 b/backend/db.sqlite3
index 2ded35b..71df3a1 100644
Binary files a/backend/db.sqlite3 and b/backend/db.sqlite3 differ
diff --git a/backend/shiftplan_backend/__pycache__/settings.cpython-314.pyc b/backend/shiftplan_backend/__pycache__/settings.cpython-314.pyc
index c47fe4e..0f844e6 100644
Binary files a/backend/shiftplan_backend/__pycache__/settings.cpython-314.pyc and b/backend/shiftplan_backend/__pycache__/settings.cpython-314.pyc differ
diff --git a/backend/shiftplan_backend/settings.py b/backend/shiftplan_backend/settings.py
index 54695bd..0ce2a90 100644
--- a/backend/shiftplan_backend/settings.py
+++ b/backend/shiftplan_backend/settings.py
@@ -71,6 +71,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'apps.users.middleware.UserActivityMiddleware',
]
ROOT_URLCONF = 'shiftplan_backend.urls'
diff --git a/frontend/dist/index.html b/frontend/dist/index.html
index 1407310..0c6b9ca 100644
--- a/frontend/dist/index.html
+++ b/frontend/dist/index.html
@@ -11,8 +11,8 @@
Schichtplaner — Veranstaltungsschichtpläne
-
-
+
+
diff --git a/frontend/src/pages/AdminPage.jsx b/frontend/src/pages/AdminPage.jsx
index 66b9d36..eebadab 100644
--- a/frontend/src/pages/AdminPage.jsx
+++ b/frontend/src/pages/AdminPage.jsx
@@ -4,13 +4,13 @@ import React, { useState, useEffect } from 'react';
import {
Shield, Settings, Users, Calendar, Layers, Palette, Plus, Trash2, Edit3,
UserCheck, UserX, Clock, CheckCircle2, AlertCircle, Layout, Search, Eye, EyeOff,
- Mail, Send, Lock
+ Mail, Send, Lock, Activity
} from 'lucide-react';
import { apiFetch } from '../api/client';
import { getRandomSkillColor } from '../utils/colors';
export default function AdminPage({ branding, onRefreshBranding, onRefreshEvents, skills, onRefreshSkills }) {
- const [activeAdminTab, setActiveAdminTab] = useState('users'); // 'users' | 'events' | 'templates' | 'branding' | 'smtp'
+ const [activeAdminTab, setActiveAdminTab] = useState('users'); // 'users' | 'events' | 'templates' | 'branding' | 'smtp' | 'sessions'
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
@@ -65,6 +65,11 @@ export default function AdminPage({ branding, onRefreshBranding, onRefreshEvents
const [savingSmtp, setSavingSmtp] = useState(false);
const [testingSmtp, setTestingSmtp] = useState(false);
+ // 6. Active Sessions & IP Monitoring State (Restricted to Session Monitor Django Group or Superuser)
+ const [activeSessions, setActiveSessions] = useState([]);
+ const [isSessionMonitorAuthorized, setIsSessionMonitorAuthorized] = useState(false);
+ const [sessionMonitorGroupName, setSessionMonitorGroupName] = useState('Session Monitor');
+
useEffect(() => {
fetchAdminData();
}, []);
@@ -118,6 +123,15 @@ export default function AdminPage({ branding, onRefreshBranding, onRefreshEvents
}
} catch (e) {}
+ try {
+ const sessionData = await apiFetch('/users/active-sessions/');
+ setActiveSessions(sessionData.sessions || []);
+ setIsSessionMonitorAuthorized(true);
+ setSessionMonitorGroupName(sessionData.group_name || 'Session Monitor');
+ } catch (e) {
+ setIsSessionMonitorAuthorized(false);
+ }
+
} catch (err) {
setError(err.message || 'Laden der Admin-Daten fehlgeschlagen.');
} finally {
@@ -157,6 +171,18 @@ export default function AdminPage({ branding, onRefreshBranding, onRefreshEvents
}
};
+ const handleConfirmUserEmail = async (userId) => {
+ try {
+ const res = await apiFetch(`/users/manage-users/${userId}/confirm-email/`, {
+ method: 'POST'
+ });
+ showNotif(res.message || 'E-Mail-Adresse manuell als bestätigt markiert.');
+ fetchAdminData();
+ } catch (err) {
+ setError(err.message || 'Fehler beim Bestätigen der E-Mail.');
+ }
+ };
+
const handleDeleteUser = async (userObj) => {
if (!window.confirm(`Möchtest du das Benutzerkonto "${userObj.display_name || userObj.username}" (@${userObj.username}) wirklich dauerhaft löschen?`)) {
return;
@@ -178,9 +204,9 @@ export default function AdminPage({ branding, onRefreshBranding, onRefreshEvents
await apiFetch(`/users/${userId}/approve/`, { method: 'POST' });
setPendingUsers(pendingUsers.filter(u => u.id !== userId));
fetchAdminData();
- showNotif('Benutzer erfolgreich freigeschaltet.');
+ showNotif('Benutzer wurde freigeschaltet!');
} catch (e) {
- setError(e.message || 'Freischalten fehlgeschlagen.');
+ setError(e.message || 'Fehler beim Freischalten.');
}
};
@@ -188,45 +214,44 @@ export default function AdminPage({ branding, onRefreshBranding, onRefreshEvents
try {
await apiFetch(`/users/${userId}/approve/`, { method: 'DELETE' });
setPendingUsers(pendingUsers.filter(u => u.id !== userId));
- showNotif('Benutzer abgelehnt und gelöscht.');
+ showNotif('Registrierungsanfrage abgelehnt.');
} catch (e) {
- setError(e.message || 'Ablehnen fehlgeschlagen.');
+ setError(e.message || 'Fehler beim Ablehnen.');
}
};
- const handleToggleRestriction = async (key, val) => {
+ // --- SECURITY SETTINGS ---
+ const handleSaveSecuritySettings = async (e) => {
+ e.preventDefault();
try {
- const payload = {
- is_restriction_enabled: key === 'restriction' ? val : restrictionEnabled,
- require_admin_approval: key === 'approval' ? val : requireApproval,
- require_email_verification: key === 'email_verify' ? val : requireEmailVerification
- };
- const res = await apiFetch('/users/restriction-setting/', {
+ await apiFetch('/users/restriction-setting/', {
method: 'POST',
- body: JSON.stringify(payload)
+ body: JSON.stringify({
+ is_restriction_enabled: restrictionEnabled,
+ require_admin_approval: requireApproval,
+ require_email_verification: requireEmailVerification
+ })
});
- setRestrictionEnabled(res.is_restriction_enabled);
- setRequireApproval(res.require_admin_approval);
- setRequireEmailVerification(res.require_email_verification);
- showNotif('Sicherheitseinstellungen aktualisiert.');
- } catch (e) {
- setError(e.message || 'Aktualisierung fehlgeschlagen.');
+ showNotif('Sicherheitseinstellungen gespeichert!');
+ } catch (err) {
+ setError(err.message || 'Speichern der Sicherheitseinstellungen fehlgeschlagen.');
}
};
const handleAddDomainRule = async (e) => {
e.preventDefault();
if (!newDomain.trim()) return;
+
try {
- const rule = await apiFetch('/users/domain-rules/', {
+ const created = await apiFetch('/users/domain-rules/', {
method: 'POST',
- body: JSON.stringify({ domain: newDomain })
+ body: JSON.stringify({ domain: newDomain.trim(), is_active: true })
});
- setDomainRules([...domainRules, rule]);
+ setDomainRules([...domainRules, created]);
setNewDomain('');
- showNotif(`Domain-Regel "${rule.domain}" hinzugefügt.`);
- } catch (e) {
- setError(e.message || 'Fehler beim Hinzufügen.');
+ showNotif(`Domain-Regel "${created.domain}" hinzugefügt.`);
+ } catch (err) {
+ setError(err.message || 'Hinzufügen der Domain-Regel fehlgeschlagen.');
}
};
@@ -235,51 +260,68 @@ export default function AdminPage({ branding, onRefreshBranding, onRefreshEvents
await apiFetch(`/users/domain-rules/${id}/`, { method: 'DELETE' });
setDomainRules(domainRules.filter(r => r.id !== id));
showNotif('Domain-Regel entfernt.');
- } catch (e) {
- setError(e.message || 'Fehler beim Löschen.');
+ } catch (err) {
+ setError(err.message || 'Fehler beim Löschen der Regel.');
}
};
- // --- SKILLS & TEMPLATES ---
+ // --- EVENTS ACTIONS ---
+ const handleDeleteEvent = async (eventObj) => {
+ if (!window.confirm(`Möchtest du die Veranstaltung "${eventObj.title}" unwiderruflich löschen?`)) return;
+
+ try {
+ await apiFetch(`/events/${eventObj.id}/`, { method: 'DELETE' });
+ setEventsList(eventsList.filter(e => e.id !== eventObj.id));
+ if (onRefreshEvents) onRefreshEvents();
+ showNotif('Veranstaltung wurde gelöscht.');
+ } catch (err) {
+ setError(err.message || 'Löschen fehlgeschlagen.');
+ }
+ };
+
+ // --- TEMPLATES & SKILLS ACTIONS ---
const handleCreateSkill = async (e) => {
e.preventDefault();
if (!newSkillName.trim()) return;
+
try {
await apiFetch('/skills/', {
method: 'POST',
body: JSON.stringify({
- name: newSkillName,
- description: newSkillDesc,
+ name: newSkillName.trim(),
+ description: newSkillDesc.trim(),
color: newSkillColor
})
});
setNewSkillName('');
setNewSkillDesc('');
setNewSkillColor(getRandomSkillColor());
- onRefreshSkills();
- showNotif('Qualifikation angelegt.');
- } catch (e) {
- setError(e.message || 'Fehler beim Anlegen.');
+ showNotif('Neue Qualifikation angelegt.');
+ if (onRefreshSkills) onRefreshSkills();
+ } catch (err) {
+ setError(err.message || 'Anlegen der Qualifikation fehlgeschlagen.');
}
};
- const handleDeleteSkill = async (id) => {
+ const handleDeleteSkill = async (skillId) => {
+ if (!window.confirm('Fähigkeit wirklich löschen?')) return;
try {
- await apiFetch(`/skills/${id}/`, { method: 'DELETE' });
- onRefreshSkills();
+ await apiFetch(`/skills/${skillId}/`, { method: 'DELETE' });
showNotif('Qualifikation gelöscht.');
- } catch (e) {
- setError(e.message || 'Fehler beim Löschen.');
+ if (onRefreshSkills) onRefreshSkills();
+ } catch (err) {
+ setError(err.message || 'Löschen fehlgeschlagen.');
}
};
- const handleDeleteTemplate = async (id) => {
+ const handleDeleteTemplate = async (templateId) => {
+ if (!window.confirm('Vorlage wirklich löschen?')) return;
try {
- await apiFetch(`/templates/${id}/`, { method: 'DELETE' });
- setTemplatesList(templatesList.filter(t => t.id !== id));
+ await apiFetch(`/templates/${templateId}/`, { method: 'DELETE' });
+ setTemplatesList(templatesList.filter(t => t.id !== templateId));
showNotif('Vorlage gelöscht.');
- } catch (e) {
- setError(e.message || 'Fehler beim Löschen.');
+ } catch (err) {
+ setError(err.message || 'Löschen der Vorlage fehlgeschlagen.');
}
};
@@ -360,24 +402,36 @@ export default function AdminPage({ branding, onRefreshBranding, onRefreshEvents
);
return (
-
- {/* Header Banner */}
-
-
-
-
-
-
-
- System Administration
-
-
- Verwaltung von Benutzern, Freischaltungen, Events, Vorlagen, Branding & SMTP-Mailserver
-
+
+ {/* Header Panel */}
+
+
+
+ [ SYSTEM-STEUERUNG & ADMINDISTRIBUTION ]
+
+ Verwaltungszentrale
+
+
+ Verwalte Konten, Sicherheit, Domain-Regeln, Vorlagen, SMTP & Live-Sitzungen
+
+ {error && (
+
+ )}
+
+ {success && (
+
+
+ {success}
+
+ )}
+
{/* Atelier Stencil Tab Bar */}
@@ -435,681 +489,727 @@ export default function AdminPage({ branding, onRefreshBranding, onRefreshEvents
>
[ 05: E-MAIL & SMTP ]
+
+ {isSessionMonitorAuthorized && (
+
+ )}
- {/* Notifications */}
- {error && (
-
-
-
{error}
+ {loading ? (
+
+ Lade Admin-Daten...
- )}
+ ) : (
+ <>
+ {/* TAB 01: BENUTZER & SICHERHEIT */}
+ {activeAdminTab === 'users' && (
+
+ {/* Pending Approvals Section */}
+ {pendingUsers.length > 0 && (
+
+
+ ⚠️ Ausstehende Registrierungen ({pendingUsers.length})
+
+
+ {pendingUsers.map(pu => (
+
+
+
{pu.display_name} (@{pu.username})
+
{pu.email}
+
+
+
+
+
+
+ ))}
+
+
+ )}
- {success && (
-
-
- {success}
-
- )}
+ {/* User Filter Search Bar */}
+
+
+ setUserSearchQuery(e.target.value)}
+ placeholder="Nutzer suchen nach Name, Username oder E-Mail..."
+ className="w-full bg-subtle border border-grid px-3 py-1.5 rounded-sm text-xs font-mono text-main focus:outline-none focus:border-muted"
+ />
+ {userSearchQuery && (
+
+ )}
+
- {/* TAB 1: BENUTZER & SICHERHEIT */}
- {activeAdminTab === 'users' && (
-
- {/* Pending Approval Section */}
- {pendingUsers.length > 0 && (
-
-
- ⚠️ Ausstehende Registrierungen ({pendingUsers.length})
+ {/* User List Table */}
+
+
+ Benutzerkonten & Rechtestatus ({filteredUsers.length})
+
+
+
+
+
+ | Nutzer |
+ E-Mail |
+ Freigeschaltet |
+ E-Mail Bestätigt |
+ Admin-Rolle |
+ Aktionen |
+
+
+
+ {filteredUsers.map(u => (
+
+ |
+ {u.display_name || u.username}
+ @{u.username}
+ |
+ {u.email} |
+
+
+ {u.is_approved ? 'JA' : 'NEIN'}
+
+ |
+
+
+
+ {u.is_email_verified ? 'VERIFIZIERT' : 'AUSSTEHEND'}
+
+ {!u.is_email_verified && (
+
+ )}
+
+ |
+
+
+ {u.is_superuser ? 'SUPERUSER' : u.is_admin_user ? 'ADMIN' : 'USER'}
+
+ |
+
+
+
+
+ |
+
+ ))}
+
+
+
+
+
+ {/* Security & Domain Restrictions */}
+
+
+ Registrierungseinschränkungen & Domains
+
+
+
+
+ {/* Domain Rules Manager */}
+
+
Erlaubte E-Mail-Domains
+
+
+
+ {domainRules.length === 0 ? (
+
[ Keine Domain-Regeln definiert. Alle E-Mail-Domains sind erlaubt. ]
+ ) : (
+ domainRules.map(r => (
+
+ {r.domain}
+
+
+ ))
+ )}
+
+
+
+
+ )}
+
+ {/* TAB 02: VERANSTALTUNGEN */}
+ {activeAdminTab === 'events' && (
+
+
+ Veranstaltungen verwalten ({eventsList.length})
- {pendingUsers.map(pu => (
-
+ {eventsList.map(evt => (
+
-
{pu.display_name} (@{pu.username})
-
{pu.email}
-
-
-
-
+
+ {evt.title}
+ {evt.is_active === false && (
+ [ DEAKTIVIERT ]
+ )}
+
+
+ {new Date(evt.start_date).toLocaleDateString('de-DE')} — {new Date(evt.end_date).toLocaleDateString('de-DE')} {evt.location ? `| Ort: ${evt.location}` : ''}
+
+
))}
)}
- {/* User Filter Search Bar */}
-
-
- setUserSearchQuery(e.target.value)}
- placeholder="Nutzer suchen nach Name, Username oder E-Mail..."
- className="w-full bg-subtle border border-grid px-3 py-1.5 rounded-sm text-xs font-mono text-main focus:outline-none focus:border-muted"
- />
- {userSearchQuery && (
-
- )}
-
+ {/* TAB 03: VORLAGEN & SKILLS */}
+ {activeAdminTab === 'templates' && (
+
+ {/* Skills Management */}
+
+
+ System-Qualifikationen & Fähigkeiten ({skills.length})
+
- {/* All Users List Table */}
-
-
- Benutzerkonten & Rechtestatus ({filteredUsers.length})
-
-
-
-
-
- | Nutzer |
- E-Mail |
- Freigeschaltet |
- E-Mail Bestätigt |
- Admin-Rolle |
- Aktionen |
-
-
-
- {filteredUsers.map(u => (
-
- |
- {u.display_name || u.username}
- @{u.username}
- |
- {u.email} |
-
-
- {u.is_approved ? 'JA' : 'NEIN'}
-
- |
-
-
- {u.is_email_verified ? 'VERIFIZIERT' : 'AUSSTEHEND'}
-
- |
-
-
- {u.is_superuser ? 'SUPERUSER' : u.is_admin_user ? 'ADMIN' : 'USER'}
-
- |
-
-
-
-
- |
-
- ))}
-
-
-
-
-
- {/* Security & Domain Restrictions */}
-
-
- Sicherheit & Registrierungs-Einschränkungen
-
-
-
-
-
- E-Mail Bestätigung (Opt-Out)
-
-
-
- Bei Pflicht muss die E-Mail vor der Anmeldung verifiziert werden. Bei Deaktivierung ist die Anmeldung direkt möglich.
-
-
-
-
-
- Domain-Einschränkung
-
-
-
- Erlaubt nur Registrierungen von festgelegten E-Mail-Domains (z. B. `@verein.de`).
-
-
-
-
-
- Admin-Freischaltpflicht
-
-
-
- Erfordert die manuelle Freischaltung durch einen Admin vor der ersten Anmeldung.
-
-
-
-
- {/* Allowed Domains List */}
- {restrictionEnabled && (
-
-
Erlaubte E-Mail-Domains
-
-
- {domainRules.map(r => (
-
- {r.domain}
-
-
- ))}
-
-
- )}
-
-
- )}
-
- {/* TAB 2: VERANSTALTUNGEN */}
- {activeAdminTab === 'events' && (
-
-
-
- Alle Veranstaltungen ({eventsList.length})
-
-
- {eventsList.map(evt => (
-
-
-
{evt.title}
-
- {new Date(evt.start_date).toLocaleDateString('de-DE')} — {evt.location || 'Kein Ort'} | Erstellt von: {evt.created_by_name || 'Admin'}
-
-
-
-
- {evt.is_active !== false ? 'AKTIV' : 'DEAKTIVIERT'}
-
-
-
-
- ))}
-
-
-
- )}
-
- {/* TAB 3: VORLAGEN & QUALIFIKATIONEN */}
- {activeAdminTab === 'templates' && (
-
- {/* Skills Management */}
-
-
- Erforderliche Qualifikationen (Skills)
-
-
-
-
-
- {skills.map(s => (
-
-
-
{s.name}
- {s.description &&
{s.description}
}
-
-
-
- ))}
-
-
-
- {/* Event Templates */}
-
-
- Gespeicherte Event-Vorlagen ({templatesList.length})
-
-
- {templatesList.map(t => (
-
-
-
{t.name}
-
{t.description || 'Keine Beschreibung'}
-
-
-
- ))}
-
-
-
- )}
-
- {/* TAB 4: BRANDING & SYSTEM */}
- {activeAdminTab === 'branding' && (
-