From 7614163f784cf9b5e955f275c4fe33bbabbbab3a Mon Sep 17 00:00:00 2001 From: Richard Date: Sat, 1 Aug 2026 12:26:44 +0200 Subject: [PATCH] feat: Implement group-restricted Live Sessions & IP Monitoring and 1-click Admin Email Confirmation --- .../users/__pycache__/admin.cpython-314.pyc | Bin 1707 -> 2454 bytes .../users/__pycache__/models.cpython-314.pyc | Bin 8420 -> 10116 bytes .../users/__pycache__/urls.cpython-314.pyc | Bin 2292 -> 2435 bytes .../users/__pycache__/views.cpython-314.pyc | Bin 25978 -> 30258 bytes backend/apps/users/admin.py | 17 +- backend/apps/users/middleware.py | 44 + .../users/migrations/0007_useractivity.py | 33 + backend/apps/users/models.py | 18 + backend/apps/users/urls.py | 3 +- backend/apps/users/views.py | 68 +- backend/db.sqlite3 | Bin 311296 -> 331776 bytes .../__pycache__/settings.cpython-314.pyc | Bin 5304 -> 5350 bytes backend/shiftplan_backend/settings.py | 1 + frontend/dist/index.html | 4 +- frontend/src/pages/AdminPage.jsx | 1420 +++++++++-------- 15 files changed, 943 insertions(+), 665 deletions(-) create mode 100644 backend/apps/users/middleware.py create mode 100644 backend/apps/users/migrations/0007_useractivity.py diff --git a/backend/apps/users/__pycache__/admin.cpython-314.pyc b/backend/apps/users/__pycache__/admin.cpython-314.pyc index 9daed993497e957d17dfb06856ea09dbfdce0f31..0ddf4d258a11c1e69f20e8b70e9aeccaca6f59ea 100644 GIT binary patch delta 1287 zcmZuw&u<$=82!dOyWX`Ohtx4KO}y*Wijq|m15zjwMFmt|KKr@1N5sQ)4*hbts@h-(6|pQ)NrM(La3x-rsa#g5QcloBIZ2Zm7$QwA zLQ`s-=lXMWpm=m!ZH{N zlP6f{g1>}{;<+a+8=ylItoOq({uW)qulUp^3jBdTf`j>bnLg$mDL~>hT7Em+@ZI>oZYcK&vE;O zs@r#{OPCk;t)A`wfL2i2{|S9B&9b@4obSZGW&{7kEQT%N5>owdgbkGSkA&;*q?x3q z3*-Wq78wMIHnA;gcT9^ukjd+Oe1Sub!y6oy{0c6pycC4niONCauzUtt{{X8}7+Cfn z;x)ABKf#Srw3_tlS-JOBlj=OcVUc|vzg73(`TY8EX5;R5v^Ed6+4r)=CcV>j*4c+9 zP-Fm&janKNN?_nRqS!zsL`B9##v~&~y_aNa6Osmr&tSl45z@oRv2HS!zk+;}yMBZc=q`B2JIflk$#Of=%eDmWG4m|r_hQU4fJ#&43`&mZ4zp?*uFi-DRqa1OSP}8&i z9r4nLKds<=XV2>Ix}~<;r^LBgvWE0lspod=u17>JkS0W8gv2?>926eQ?p)j1snly` zwO*@yQa8*kqkf|i#NC^FcAI*^%&tvMmzdFI3Y2SKwCw?9*ENudJ=%3V%ClGHtA#0! zu1;9$@KDnS4dnOPUWQ%!3i*Jo&R?!~%3v%YgifGv0w12h`ft#B46Wxm{TQ-iNkGao ztQ=Iosy@ZqBdi^-ymySXC;0MM79bY)OOp5LIEGMZEHQeZ-cj$T9;5t=n1F`pJMn35 y?NM&+6gWF&2KCP{@Ynbv$a60MA>~&r^YxX#y*aj(AD6DGaHPhst_er0h~d931tv)V delta 635 zcmbOxyqcF!n~#@^0SLbOJ1NUc&CdQNkh55X%@K z1X76#6d7XKOc)_zq6~@*K`i17L9DSXCd?2q6R7eMW}N!jQS`F`^|P}86^l&X$EeK8 z4bnDol`P@EGctiuz;rOcqo{SsulCgUyE#FX63JRpNJv^cfM5zJw}#RB5o5}SOH zMQpMglfvX)CRIk($(xzxu<-!374c0DWuC;yJ^2N5B2d_i#f(vUawCf_BhTdZEPf%ti7br3yge7FTjnYGO%hN_=98CP$GlNL&O&h=K?)5Ft6)lvTk=3d96yFOmik zG9W?@Mu2QC2I**HcqDAm;B!M*Zh~%u_YG0m30^I}5FK)pr?YA>3Qa!18W0C@1{-$k zK!#$oO%)^oB0v#V42s`2h6f@#7ld_Nd>~eUB>gmbCr@BElNJZEZ?Wa(r=;c-7qJ34 zMQk8~9Y{=eXM&57%Q6rQoy$@(Y$%f^mzQ*WB6>9!;hZPZ+%rfvc>G%3}l4OOApI`%3xP1n)v zIK>vKEkGru2NI1$L6zvCJs1uI#3hIW1y0;dWXaZw-nfE_8c}ZWW}UxEjMv}3neV+f z?|U=*d-8+V{YU(59t7jeFW29G(7x*L#N*%e#jvLXbt5$tMQWJIIMM1?aSu}3zkLON zwEs|=2W$^<%XoSVBa}jAbQw{sBI=mE#2&|oAUYCJG5j2=bJ*2%rtZtaIf>yeDKh(s zO$x)#Ey}x1Nwfa~^ zkge4XT0KMCn}tK+o(B4u9rgWMrdiobqCe=(`H}YL2NOcMfTGUB@ z@|rE_=Z91tl&7<_m^bG0-T&{Q8@oQbK@Nh~Sw$p$@Fs^iC>#z0SYnQ3bdojZE#Hz( z=5nU48L8{K)ef`iovF6CkSD1H@)D2Z=|~rc7kCDN7f$Yi$s1xy0Jl&>l2=YFi;d{< zWnW`|vL?ox5%<_*A%=fqqwYBS)9V)E2<>CZV3hqKNz8DIj;RjzRY!!aIlUpcZTnCH zsb~?;U>{O3Gh8v3V+NfQ*_EJFI?B7~w1iioV{e@$WaaQ1ZO@65k%C;iHkI<5yI~+}eJv8kqFy*Lv?`9W+mXJv-h-@!e|zM8085;H`Mh@a!sKLA%!{0`#duASkyX^w;#!P%U)2{8}U2Sq~rB2p_1V*T-w&WckdlmkM5L z(`z?!U3fHC3r#>|Fj5c78$r1;X8Q$CmnZDC!QPEvZzWR;9xG2g6P&J7_%FoqnaANe ziJu^j&HKaaR|0G!Vc=CZB6oSdga!WXnRi%PUh3LJZajuJBSCx;uN=QSvO4lN;)FZ&nN<66v7ybVT`McLd+roU{&l)Ld@I{rX`Rm2w`Re znL+`2lN*@ynRy^W=Yc}JV5X>i5Vtuq!~wC4iVQ&tvHTD<#>_&@0$`@3GMXBdSSCdV zpj!0Dt=Uv&2FrUjEqu~GuRsWi$s8~y2VzMUs{q{R3tX}Gh02EDo}_Kh>LqC w*RU&3p2faUSo(sj{&iXNOS0ydWi2l_1YThYnrz0gnOl~hpQ(wvNExUa0M;97g#Z8m delta 322 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 delta 5436 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 delta 286 zcmZozAkxqvJV9EpfPsO*2Z&*yX`+rXW5LFRCH&0FY>y|i3%GAKRG7-P`LS4~1Y^MD zSp85&j>((#O(jG`8JYDNQ?e5C((~hUGt-L_OEUBGij_ARuoN(h@aauvU!b{J(BL2M zX1#US7?}e&CQfGGun=h2OZLqZcNDV&)#y%UKLAqmmv^)7x$B0^0eq@c*%?7e)U;yJ32L3zzoA^7X^A#}8nj9$b zX8V+nEIE9PT$>paSOnTxColpr({|Pg%>UoZ@USdn;61|I&l|a=4RP! 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 && ( +
+ + {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}) +

+
+ + + + + + + + + + + + + {filteredUsers.map(u => ( + + + + + + + + + ))} + +
NutzerE-MailFreigeschaltetE-Mail BestätigtAdmin-RolleAktionen
+ {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-Einschränkung aktivieren
+
Nur E-Mails aus erlaubten Domains zulassen
+
+ setRestrictionEnabled(e.target.checked)} + className="w-4 h-4 accent-indigo-500 cursor-pointer" + /> +
+ +
+
+
Manuelle Admin-Freischaltung erzwingen
+
Neue Konten sind erst nach Admin-Freigabe aktiv
+
+ setRequireApproval(e.target.checked)} + className="w-4 h-4 accent-indigo-500 cursor-pointer" + /> +
+ +
+
+
E-Mail-Bestätigung erzwingen
+
Benutzer müssen ihre E-Mail-Adresse per Link verifizieren
+
+ setRequireEmailVerification(e.target.checked)} + className="w-4 h-4 accent-indigo-500 cursor-pointer" + /> +
+ +
+ +
+
+ + {/* Domain Rules Manager */} +
+

Erlaubte E-Mail-Domains

+
+ setNewDomain(e.target.value)} + className="flex-1 bg-subtle border border-grid px-3 py-1.5 rounded-sm text-xs font-mono" + /> + +
+ +
+ {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}) -

-
- - - - - - - - - - - - - {filteredUsers.map(u => ( - - - - - - - - - ))} - -
NutzerE-MailFreigeschaltetE-Mail BestätigtAdmin-RolleAktionen
- {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

-
+ setNewDomain(e.target.value)} - className="flex-1 px-3 py-1.5 rounded-sm input-field text-xs font-mono" + required + placeholder="Name der Qualifikation (z.B. Baristakurs)" + value={newSkillName} + onChange={(e) => setNewSkillName(e.target.value)} + className="sm:col-span-5 bg-subtle border border-grid px-3 py-1.5 rounded-sm text-xs font-mono" + /> + setNewSkillDesc(e.target.value)} + className="sm:col-span-4 bg-subtle border border-grid px-3 py-1.5 rounded-sm text-xs font-mono" /> - -
-
- {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) -

- -
- setNewSkillName(e.target.value)} - className="px-3 py-1.5 rounded-sm input-field text-xs font-mono" - /> - setNewSkillDesc(e.target.value)} - className="px-3 py-1.5 rounded-sm input-field text-xs font-mono" - /> -
- setNewSkillColor(e.target.value)} - className="w-8 h-8 rounded-sm border-0 cursor-pointer bg-subtle p-0.5" - /> - -
-
- -
- {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' && ( -
-
-

- Portal Branding & Erscheinungsbild -

- -
-
- - setAppName(e.target.value)} - className="w-full px-3 py-1.5 rounded-sm input-field font-bold" - /> -
- -
- -
setPrimaryColor(e.target.value)} - className="w-9 h-9 rounded-sm border-0 cursor-pointer bg-subtle p-0.5" + value={newSkillColor} + onChange={(e) => setNewSkillColor(e.target.value)} + className="sm:col-span-1 h-8 w-full cursor-pointer bg-subtle p-0.5 border border-grid rounded-sm" /> + + + +
+ {skills.map(sk => ( +
+ {sk.name} + +
+ ))} +
+
+ + {/* Templates Management */} +
+

+ Veranstaltungsvorlagen ({templatesList.length}) +

+
+ {templatesList.map(t => ( +
+
+
{t.name}
+
{t.description || '[ Keine Beschreibung ]'}
+
+ +
+ ))} +
+
+
+ )} + + {/* TAB 04: BRANDING & SYSTEM */} + {activeAdminTab === 'branding' && ( +
+

+ Branding & Design-Einstellungen +

+ +
+
+ setPrimaryColor(e.target.value)} - className="flex-1 px-3 py-1.5 rounded-sm input-field font-mono" + value={appName} + onChange={(e) => setAppName(e.target.value)} + className="w-full px-3 py-2 rounded-sm input-field text-xs" + /> +
+ +
+ + setLogoUrl(e.target.value)} + placeholder="https://..." + className="w-full px-3 py-2 rounded-sm input-field text-xs" + /> +
+ +
+ +
+ setPrimaryColor(e.target.value)} + className="h-9 w-12 rounded-sm cursor-pointer bg-subtle p-1 border border-grid" + /> + setPrimaryColor(e.target.value)} + className="flex-1 px-3 py-2 rounded-sm input-field text-xs font-mono" + /> +
+
+ +
+ + setBannerText(e.target.value)} + placeholder="z. B. Willkommen zum Sommerfest-Schichtplan!" + className="w-full px-3 py-2 rounded-sm input-field text-xs" />
-
- - setLogoUrl(e.target.value)} - className="w-full px-3 py-1.5 rounded-sm input-field" - /> -
- -
- -