feat(admin): restrict admin access and enhance navigation for superadmins
All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 8s
Test Suite / playwright-tests (push) Successful in 23s
Test Suite / pytest-backend (pull_request) Successful in 24s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 7s
Test Suite / playwright-tests (pull_request) Successful in 24s
All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 8s
Test Suite / playwright-tests (push) Successful in 23s
Test Suite / pytest-backend (pull_request) Successful in 24s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 7s
Test Suite / playwright-tests (pull_request) Successful in 24s
- Updated access control to ensure only superadmins can view admin routes and manage users. - Refactored navigation components to reflect the new role-based access, removing platform admin references. - Enhanced the admin user management page to streamline functionality for superadmins, including password reset options. - Improved overall user experience by clarifying navigation paths and access permissions for different user roles.
This commit is contained in:
parent
30c1c259d2
commit
01be9ffcd4
|
|
@ -39,14 +39,11 @@ import PlatformAdminRoute from './components/PlatformAdminRoute'
|
||||||
import MediaLibraryPage from './pages/MediaLibraryPage'
|
import MediaLibraryPage from './pages/MediaLibraryPage'
|
||||||
import ActiveClubSwitcher from './components/ActiveClubSwitcher'
|
import ActiveClubSwitcher from './components/ActiveClubSwitcher'
|
||||||
import InactiveMembershipBanner from './components/InactiveMembershipBanner'
|
import InactiveMembershipBanner from './components/InactiveMembershipBanner'
|
||||||
import { activeClubMemberships } from './utils/activeClub'
|
|
||||||
import './app.css'
|
import './app.css'
|
||||||
|
|
||||||
/** Shield-„Admin“: Portal-Admins oder Vereinsorganisation (Zugriff mindestens /admin/users). */
|
/** Shield „Admin“: nur Super-Admin (global). Vereinsorga: Vereine → Mitglieder. */
|
||||||
function computeShowAdminNav(currentUser) {
|
function computeShowAdminNav(currentUser) {
|
||||||
const plat = currentUser?.role === 'admin' || currentUser?.role === 'superadmin'
|
return currentUser?.role === 'superadmin'
|
||||||
if (plat) return true
|
|
||||||
return activeClubMemberships(currentUser?.clubs).some((c) => (c.roles || []).includes('club_admin'))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottom Navigation (Mobile)
|
// Bottom Navigation (Mobile)
|
||||||
|
|
@ -196,7 +193,14 @@ function AppRoutes() {
|
||||||
<Route path="planning/run/:unitId" element={<TrainingUnitRunPage />} />
|
<Route path="planning/run/:unitId" element={<TrainingUnitRunPage />} />
|
||||||
<Route path="planning" element={<TrainingPlanningPage />} />
|
<Route path="planning" element={<TrainingPlanningPage />} />
|
||||||
<Route path="admin" element={<AdminHomeRedirect />} />
|
<Route path="admin" element={<AdminHomeRedirect />} />
|
||||||
<Route path="admin/users" element={<AdminUsersPage />} />
|
<Route
|
||||||
|
path="admin/users"
|
||||||
|
element={
|
||||||
|
<PlatformAdminRoute>
|
||||||
|
<AdminUsersPage />
|
||||||
|
</PlatformAdminRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="admin/hierarchy"
|
path="admin/hierarchy"
|
||||||
element={
|
element={
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@ import { useAuth } from '../context/AuthContext'
|
||||||
|
|
||||||
export default function AdminHomeRedirect() {
|
export default function AdminHomeRedirect() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const isPlat = user?.role === 'admin' || user?.role === 'superadmin'
|
const isSuper = user?.role === 'superadmin'
|
||||||
return <Navigate to={isPlat ? '/admin/hierarchy' : '/admin/users'} replace />
|
return <Navigate to={isSuper ? '/admin/hierarchy' : '/'} replace />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,10 @@ import { NavLink } from 'react-router-dom'
|
||||||
import { TreePine, FolderTree, Download, Grid3x3, Users } from 'lucide-react'
|
import { TreePine, FolderTree, Download, Grid3x3, Users } from 'lucide-react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin-Seiten-Navigation (horizontal)
|
* Admin-Seiten-Navigation (horizontal) — nur für Super-Admins (globaler Portal-Mandant).
|
||||||
* Nutzer-Verwaltung: eingeschränkte Tabs für Vereinsorga ohne Plattform-Admin.
|
|
||||||
*/
|
*/
|
||||||
export default function AdminPageNav({ clubOrgOnly = false }) {
|
export default function AdminPageNav() {
|
||||||
const pages = clubOrgOnly
|
const pages = [
|
||||||
? [{ to: '/admin/users', label: 'Nutzer', icon: Users }]
|
|
||||||
: [
|
|
||||||
{ to: '/admin/hierarchy', label: 'Hierarchie', icon: TreePine },
|
{ to: '/admin/hierarchy', label: 'Hierarchie', icon: TreePine },
|
||||||
{ to: '/admin/users', label: 'Nutzer', icon: Users },
|
{ to: '/admin/users', label: 'Nutzer', icon: Users },
|
||||||
{ to: '/admin/maturity-models', label: 'Fähigkeitsmatrix', icon: Grid3x3 },
|
{ to: '/admin/maturity-models', label: 'Fähigkeitsmatrix', icon: Grid3x3 },
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Navigate } from 'react-router-dom'
|
import { Navigate } from 'react-router-dom'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
|
||||||
/** Nur Plattform-Admins (admin/superadmin); Vereinsorga → /admin/users */
|
/** Nur Super-Admins; andere Nutzer → Startseite. */
|
||||||
export default function PlatformAdminRoute({ children }) {
|
export default function PlatformAdminRoute({ children }) {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const ok = user?.role === 'admin' || user?.role === 'superadmin'
|
const ok = user?.role === 'superadmin'
|
||||||
if (!ok) return <Navigate to="/admin/users" replace />
|
if (!ok) return <Navigate to="/" replace />
|
||||||
return children
|
return children
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { Navigate } from 'react-router-dom'
|
import { Navigate } from 'react-router-dom'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import { activeClubMemberships } from '../utils/activeClub'
|
|
||||||
import AdminPageNav from '../components/AdminPageNav'
|
import AdminPageNav from '../components/AdminPageNav'
|
||||||
|
|
||||||
const CLUB_ROLE_OPTIONS = [
|
const CLUB_ROLE_OPTIONS = [
|
||||||
|
|
@ -19,26 +18,6 @@ const PORTAL_ROLE_LABEL = {
|
||||||
superadmin: 'Super-Administrator',
|
superadmin: 'Super-Administrator',
|
||||||
}
|
}
|
||||||
|
|
||||||
function clubAdminClubIds(user) {
|
|
||||||
return activeClubMemberships(user?.clubs)
|
|
||||||
.filter((c) => Array.isArray(c.roles) && c.roles.includes('club_admin'))
|
|
||||||
.map((c) => c.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function clubSelectOptions(user, allClubs, isPlatformAdmin) {
|
|
||||||
if (!isPlatformAdmin) {
|
|
||||||
const ids = new Set(clubAdminClubIds(user))
|
|
||||||
return (allClubs || []).filter((c) => ids.has(c.id))
|
|
||||||
}
|
|
||||||
return allClubs || []
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Plattform-Rollen im UI (Tier/Abo entfällt bis auf Weiteres). */
|
|
||||||
function isEscalatedPortalRole(role) {
|
|
||||||
const r = (role || 'user').toLowerCase()
|
|
||||||
return r === 'admin' || r === 'superadmin'
|
|
||||||
}
|
|
||||||
|
|
||||||
function portalRoleSelectOptions(viewerIsSuperadmin, currentRole) {
|
function portalRoleSelectOptions(viewerIsSuperadmin, currentRole) {
|
||||||
const base = [
|
const base = [
|
||||||
{ value: 'user', label: PORTAL_ROLE_LABEL.user },
|
{ value: 'user', label: PORTAL_ROLE_LABEL.user },
|
||||||
|
|
@ -54,45 +33,26 @@ function portalRoleSelectOptions(viewerIsSuperadmin, currentRole) {
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nur Super-Admins — globale Nutzer- und Portal-Rollen plus Vereinszuordnung pro Profil.
|
||||||
|
* Vereinsorganisation (Mitglieder, Anträge, Rollen vor Ort): unter Vereine → Mitglieder.
|
||||||
|
*/
|
||||||
export default function AdminUsersPage() {
|
export default function AdminUsersPage() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const isPlatformAdmin = user?.role === 'admin' || user?.role === 'superadmin'
|
|
||||||
const isSuperadminViewer = user?.role === 'superadmin'
|
const isSuperadminViewer = user?.role === 'superadmin'
|
||||||
const managedClubIds = useMemo(() => clubAdminClubIds(user), [user])
|
|
||||||
const clubOrgMode = !isPlatformAdmin && managedClubIds.length > 0
|
|
||||||
const canAccess = isPlatformAdmin || clubOrgMode
|
|
||||||
|
|
||||||
const [platformUsers, setPlatformUsers] = useState([])
|
const [platformUsers, setPlatformUsers] = useState([])
|
||||||
const [clubs, setClubs] = useState([])
|
const [clubs, setClubs] = useState([])
|
||||||
const [clubMembers, setClubMembers] = useState([])
|
|
||||||
const [selectedClubId, setSelectedClubId] = useState(
|
|
||||||
() => managedClubIds[0] ?? null
|
|
||||||
)
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [portalDraft, setPortalDraft] = useState({})
|
const [portalDraft, setPortalDraft] = useState({})
|
||||||
const [assignModal, setAssignModal] = useState(null)
|
const [assignModal, setAssignModal] = useState(null)
|
||||||
const [assignRoles, setAssignRoles] = useState(['trainer'])
|
const [assignRoles, setAssignRoles] = useState(['trainer'])
|
||||||
const [clubEditModal, setClubEditModal] = useState(null)
|
const [clubEditModal, setClubEditModal] = useState(null)
|
||||||
const [addMemberOpen, setAddMemberOpen] = useState(false)
|
|
||||||
const [newMemberProfileId, setNewMemberProfileId] = useState('')
|
|
||||||
const [newMemberRoles, setNewMemberRoles] = useState(['trainer'])
|
|
||||||
const [pwdModal, setPwdModal] = useState(null)
|
const [pwdModal, setPwdModal] = useState(null)
|
||||||
const [pwdNew, setPwdNew] = useState('')
|
const [pwdNew, setPwdNew] = useState('')
|
||||||
const [pwdNew2, setPwdNew2] = useState('')
|
const [pwdNew2, setPwdNew2] = useState('')
|
||||||
|
|
||||||
const selectableClubs = useMemo(
|
|
||||||
() => clubSelectOptions(user, clubs, isPlatformAdmin),
|
|
||||||
[user, clubs, isPlatformAdmin]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!clubOrgMode) return
|
|
||||||
if (selectedClubId == null || !managedClubIds.includes(selectedClubId)) {
|
|
||||||
setSelectedClubId(managedClubIds[0] ?? null)
|
|
||||||
}
|
|
||||||
}, [clubOrgMode, managedClubIds, selectedClubId])
|
|
||||||
|
|
||||||
const loadPlatform = useCallback(async () => {
|
const loadPlatform = useCallback(async () => {
|
||||||
const [u, c] = await Promise.all([api.listAdminUsers(), api.listClubs()])
|
const [u, c] = await Promise.all([api.listAdminUsers(), api.listClubs()])
|
||||||
setPlatformUsers(Array.isArray(u) ? u : [])
|
setPlatformUsers(Array.isArray(u) ? u : [])
|
||||||
|
|
@ -105,8 +65,7 @@ export default function AdminUsersPage() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canAccess) return
|
if (!isSuperadminViewer) return
|
||||||
if (clubOrgMode) return
|
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
;(async () => {
|
;(async () => {
|
||||||
setError('')
|
setError('')
|
||||||
|
|
@ -122,48 +81,9 @@ export default function AdminUsersPage() {
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [canAccess, clubOrgMode, loadPlatform])
|
}, [isSuperadminViewer, loadPlatform])
|
||||||
|
|
||||||
useEffect(() => {
|
if (!isSuperadminViewer) return <Navigate to="/" replace />
|
||||||
if (!canAccess || !clubOrgMode || !selectedClubId) return
|
|
||||||
let cancelled = false
|
|
||||||
;(async () => {
|
|
||||||
setError('')
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const [c, m] = await Promise.all([
|
|
||||||
api.listClubs(),
|
|
||||||
api.listClubMembers(selectedClubId, { includeInactive: true }),
|
|
||||||
])
|
|
||||||
if (!cancelled) {
|
|
||||||
setClubs(Array.isArray(c) ? c : [])
|
|
||||||
setClubMembers(Array.isArray(m) ? m : [])
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (!cancelled) setError(e.message || String(e))
|
|
||||||
} finally {
|
|
||||||
if (!cancelled) setLoading(false)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [canAccess, clubOrgMode, selectedClubId])
|
|
||||||
|
|
||||||
const reloadClubMembers = useCallback(async () => {
|
|
||||||
if (!selectedClubId) return
|
|
||||||
try {
|
|
||||||
const m = await api.listClubMembers(selectedClubId, { includeInactive: true })
|
|
||||||
setClubMembers(Array.isArray(m) ? m : [])
|
|
||||||
} catch {
|
|
||||||
setClubMembers([])
|
|
||||||
}
|
|
||||||
}, [selectedClubId])
|
|
||||||
|
|
||||||
if (!canAccess) return <Navigate to="/" replace />
|
|
||||||
|
|
||||||
const selectedClubLabel =
|
|
||||||
selectableClubs.find((c) => c.id === selectedClubId)?.name || 'Verein'
|
|
||||||
|
|
||||||
const savePortal = async (profileId) => {
|
const savePortal = async (profileId) => {
|
||||||
const dr = portalDraft[profileId]
|
const dr = portalDraft[profileId]
|
||||||
|
|
@ -200,31 +120,7 @@ export default function AdminUsersPage() {
|
||||||
try {
|
try {
|
||||||
await api.updateClubMember(clubId, profileId, { roles, status })
|
await api.updateClubMember(clubId, profileId, { roles, status })
|
||||||
setClubEditModal(null)
|
setClubEditModal(null)
|
||||||
if (clubOrgMode) await reloadClubMembers()
|
await loadPlatform()
|
||||||
else await loadPlatform()
|
|
||||||
} catch (e) {
|
|
||||||
alert(e.message || String(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleMemberClubAccess = async (m, activate) => {
|
|
||||||
if (!selectedClubId) return
|
|
||||||
const st = activate ? 'active' : 'inactive'
|
|
||||||
if (
|
|
||||||
!activate &&
|
|
||||||
!confirm(
|
|
||||||
`Vereinszugang für „${m.name || m.email || '#' + m.profile_id}“ in ${selectedClubLabel} deaktivieren? ` +
|
|
||||||
'Die Person bleibt anmeldbar, sieht aber keine Inhalte dieses Vereins mehr (Login bleibt unverändert).'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await api.updateClubMember(selectedClubId, m.profile_id, {
|
|
||||||
roles: [...(m.roles || [])],
|
|
||||||
status: st,
|
|
||||||
})
|
|
||||||
await reloadClubMembers()
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e.message || String(e))
|
alert(e.message || String(e))
|
||||||
}
|
}
|
||||||
|
|
@ -236,30 +132,7 @@ export default function AdminUsersPage() {
|
||||||
try {
|
try {
|
||||||
await api.removeClubMember(clubEditModal.clubId, clubEditModal.profileId)
|
await api.removeClubMember(clubEditModal.clubId, clubEditModal.profileId)
|
||||||
setClubEditModal(null)
|
setClubEditModal(null)
|
||||||
if (clubOrgMode) await reloadClubMembers()
|
await loadPlatform()
|
||||||
else await loadPlatform()
|
|
||||||
} catch (e) {
|
|
||||||
alert(e.message || String(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitAddClubMember = async () => {
|
|
||||||
const raw = parseInt(String(newMemberProfileId).trim(), 10)
|
|
||||||
if (!Number.isFinite(raw) || raw < 1) {
|
|
||||||
alert('Gültige Profil-ID eingeben.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!selectedClubId) return
|
|
||||||
if (!newMemberRoles.length) {
|
|
||||||
alert('Mindestens eine Vereinsrolle.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await api.addClubMember(selectedClubId, { profile_id: raw, roles: newMemberRoles })
|
|
||||||
setAddMemberOpen(false)
|
|
||||||
setNewMemberProfileId('')
|
|
||||||
setNewMemberRoles(['trainer'])
|
|
||||||
await reloadClubMembers()
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e.message || String(e))
|
alert(e.message || String(e))
|
||||||
}
|
}
|
||||||
|
|
@ -284,7 +157,7 @@ export default function AdminUsersPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitPasswordDirect = async () => {
|
const submitPasswordDirect = async () => {
|
||||||
if (!pwdModal || !isSuperadminViewer) return
|
if (!pwdModal) return
|
||||||
if (pwdNew.length < 8) {
|
if (pwdNew.length < 8) {
|
||||||
alert('Mindestens 8 Zeichen.')
|
alert('Mindestens 8 Zeichen.')
|
||||||
return
|
return
|
||||||
|
|
@ -306,23 +179,15 @@ export default function AdminUsersPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page">
|
<div className="app-page">
|
||||||
<AdminPageNav clubOrgOnly={clubOrgMode} />
|
<AdminPageNav />
|
||||||
|
|
||||||
<h1 style={{ marginTop: 0 }}>Nutzer & Vereinsrollen</h1>
|
<h1 style={{ marginTop: 0 }}>Globale Nutzer & Portal-Rollen</h1>
|
||||||
|
|
||||||
{clubOrgMode ? (
|
|
||||||
<p style={{ color: 'var(--text2)', maxWidth: '52rem', lineHeight: 1.55, marginBottom: '1.25rem' }}>
|
|
||||||
Du verwaltest Mitglieder des ausgewählten Vereins. <strong>Vereinszugang deaktivieren</strong> sperrt nur die
|
|
||||||
Sicht auf Vereinsinhalte — der Login des Nutzers bleibt möglich. Wiederherstellen über „aktivieren“ oder
|
|
||||||
Bearbeiten.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<p style={{ color: 'var(--text2)', maxWidth: '52rem', lineHeight: 1.55, marginBottom: '0.75rem' }}>
|
<p style={{ color: 'var(--text2)', maxWidth: '52rem', lineHeight: 1.55, marginBottom: '0.75rem' }}>
|
||||||
Gesamtübersicht aller Konten und Vereinszuordnungen. <strong>Portal</strong>-Einstellungen steuern nur den
|
Plattformweite Konten und Vereinszuordnungen im Überblick. <strong>Vereinsmitgliedschaft vor Ort</strong>{' '}
|
||||||
Zugang zur <strong>plattformweiten Administration</strong> (Kataloge, Hierarchie, globale Nutzerliste usw.);
|
(Mitglieder, Beitrittsanträge, Vereinszugriffe) liegt unter <strong>Vereine → Mitglieder</strong>. Die
|
||||||
Vereinsarbeit (Trainer, Vereinsadmin …) bleiben <strong>Vereinsrollen</strong>. Abonnement/Tier ist
|
geschützten <strong>/admin</strong>-Menüpunkte (Hierarchie, Kataloge, …) gibt es nur noch hier für Super-Admins.
|
||||||
derzeit nicht freigeschaltet.
|
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
className="card"
|
className="card"
|
||||||
|
|
@ -335,44 +200,25 @@ export default function AdminUsersPage() {
|
||||||
color: 'var(--text2)',
|
color: 'var(--text2)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong style={{ color: 'var(--text1)' }}>Die vier Portal-Zugriffsstufen:</strong>
|
<strong style={{ color: 'var(--text1)' }}>Portal-Rollen auf Profil-Ebene:</strong>
|
||||||
<ul style={{ margin: '0.5rem 0 0', paddingLeft: '1.2rem' }}>
|
<ul style={{ margin: '0.5rem 0 0', paddingLeft: '1.2rem' }}>
|
||||||
<li>
|
<li>
|
||||||
<strong>Nutzer</strong> — Standardnutzer ohne Plattform-Admin-Bereiche.
|
<strong>Nutzer</strong> — Standard ohne globale Administrationsbereiche.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Portal-Trainer (Legacy)</strong> — ältere Kennzeichnung auf Profil-Ebene; organisatorisch ist
|
<strong>Portal-Trainer (Legacy)</strong> — historische Kennzeichnung; organisatorisch ist „Trainer“ meist eine{' '}
|
||||||
„Trainer“ in der Regel eine <strong>Vereinsrolle</strong>.
|
<strong>Vereinsrolle</strong>.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Portal-Administrator</strong> — Zugang zu allen geschützten <code>/admin</code>-Bereichen
|
<strong>Portal-Administrator</strong> — erhöhte API-/Operativ-Rechte nach Backend-Policies; die{' '}
|
||||||
(Außer: einige Funktionen nur Superadmin, z. B. bestimmte Medien-/Governance-Aktionen).
|
<strong>Admin-Navigation dieser App</strong> sieht jedoch nur noch der Super-Admin.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Super-Administrator</strong> — volle Plattform-Governance (u. a. offizielle Medien,
|
<strong>Super-Administrator</strong> — volle Plattform-Governance (diese Oberfläche, offizielle Inhalte, …).
|
||||||
Superadmin-Rolle vergeben, harte Lifecycle-Aktionen an Medien).
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
|
|
||||||
{clubOrgMode && managedClubIds.length > 1 ? (
|
|
||||||
<div className="form-row" style={{ maxWidth: '24rem', marginBottom: '1rem' }}>
|
|
||||||
<label className="form-label">Verein für Verwaltung</label>
|
|
||||||
<select
|
|
||||||
className="form-input"
|
|
||||||
value={selectedClubId ?? ''}
|
|
||||||
onChange={(e) => setSelectedClubId(parseInt(e.target.value, 10))}
|
|
||||||
>
|
|
||||||
{selectableClubs.map((c) => (
|
|
||||||
<option key={c.id} value={c.id}>
|
|
||||||
{c.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p style={{ color: 'var(--text2)' }}>Laden…</p>
|
<p style={{ color: 'var(--text2)' }}>Laden…</p>
|
||||||
|
|
@ -380,98 +226,6 @@ export default function AdminUsersPage() {
|
||||||
<div className="card" style={{ borderColor: 'var(--danger)', color: 'var(--danger)' }}>
|
<div className="card" style={{ borderColor: 'var(--danger)', color: 'var(--danger)' }}>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
) : clubOrgMode ? (
|
|
||||||
<div style={{ display: 'grid', gap: '1rem' }}>
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', alignItems: 'center' }}>
|
|
||||||
<button type="button" className="btn btn-primary" onClick={() => setAddMemberOpen(true)}>
|
|
||||||
Mitglied hinzufügen (Profil-ID)
|
|
||||||
</button>
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => reloadClubMembers()}>
|
|
||||||
Aktualisieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{!clubMembers.length ? (
|
|
||||||
<p className="muted">Keine Mitglieder in diesem Verein.</p>
|
|
||||||
) : (
|
|
||||||
clubMembers.map((m) => {
|
|
||||||
const memStatus = (m.status || 'active').toLowerCase()
|
|
||||||
return (
|
|
||||||
<div key={m.membership_id} className="card">
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: '0.75rem' }}>
|
|
||||||
<div>
|
|
||||||
<strong style={{ fontSize: '1.05rem' }}>
|
|
||||||
{m.name || '—'} <span style={{ color: 'var(--text2)', fontWeight: 400 }}>#{m.profile_id}</span>
|
|
||||||
</strong>
|
|
||||||
<div style={{ fontSize: '0.875rem', color: 'var(--text2)' }}>{m.email || '—'}</div>
|
|
||||||
<div style={{ fontSize: '0.78rem', color: 'var(--text3)', marginTop: '0.35rem' }}>
|
|
||||||
Vereinszugang:{' '}
|
|
||||||
<strong style={{ color: memStatus === 'active' ? 'var(--text1)' : 'var(--warning, #d4a012)' }}>
|
|
||||||
{memStatus === 'active' ? 'aktiv' : 'deaktiviert'}
|
|
||||||
</strong>
|
|
||||||
{' '}
|
|
||||||
· Verifiziert: {m.email_verified ? 'ja' : 'nein'}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '0.82rem', marginTop: '0.35rem' }}>
|
|
||||||
Rollen: {(m.roles || []).join(', ') || '—'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem', alignItems: 'stretch' }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={() =>
|
|
||||||
setClubEditModal({
|
|
||||||
clubId: selectedClubId,
|
|
||||||
clubName: selectedClubLabel,
|
|
||||||
profileId: m.profile_id,
|
|
||||||
profileLabel: m.name || m.email,
|
|
||||||
roles: [...(m.roles || [])],
|
|
||||||
status: memStatus,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Bearbeiten
|
|
||||||
</button>
|
|
||||||
{m.profile_id !== user?.id ? (
|
|
||||||
memStatus === 'inactive' ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={() => toggleMemberClubAccess(m, true)}
|
|
||||||
>
|
|
||||||
Vereinszugang aktivieren
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={() => toggleMemberClubAccess(m, false)}
|
|
||||||
>
|
|
||||||
Vereinszugang deaktivieren
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
{m.profile_id !== user?.id && !isEscalatedPortalRole(m.portal_role) ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={() =>
|
|
||||||
setPwdModal({
|
|
||||||
profileId: m.profile_id,
|
|
||||||
label: m.name || m.email || `Profil #${m.profile_id}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Passwort-Link senden
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'grid', gap: '1rem' }}>
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||||
{platformUsers.map((row) => {
|
{platformUsers.map((row) => {
|
||||||
|
|
@ -591,7 +345,7 @@ export default function AdminUsersPage() {
|
||||||
>
|
>
|
||||||
bearbeiten
|
bearbeiten
|
||||||
</button>
|
</button>
|
||||||
{isSuperadminViewer && c.membership_status === 'inactive' ? (
|
{c.membership_status === 'inactive' ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -706,77 +460,6 @@ export default function AdminUsersPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{addMemberOpen && clubOrgMode ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
inset: 0,
|
|
||||||
background: 'rgba(0,0,0,0.5)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
zIndex: 1200,
|
|
||||||
padding: '1rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: 'var(--surface)',
|
|
||||||
borderRadius: '12px',
|
|
||||||
padding: '1.5rem',
|
|
||||||
maxWidth: '440px',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2 style={{ marginTop: 0 }}>Mitglied hinzufügen</h2>
|
|
||||||
<p className="muted" style={{ fontSize: '0.9rem' }}>
|
|
||||||
Verein: <strong>{selectedClubLabel}</strong>
|
|
||||||
</p>
|
|
||||||
<div className="form-row">
|
|
||||||
<label className="form-label">Profil-ID</label>
|
|
||||||
<input
|
|
||||||
className="form-input"
|
|
||||||
inputMode="numeric"
|
|
||||||
value={newMemberProfileId}
|
|
||||||
onChange={(e) => setNewMemberProfileId(e.target.value)}
|
|
||||||
placeholder="z. B. 42"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-row">
|
|
||||||
<span className="form-label">Rollen</span>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
|
||||||
{CLUB_ROLE_OPTIONS.map((opt) => (
|
|
||||||
<label key={opt.code} style={{ display: 'flex', alignItems: 'center', gap: '0.35rem' }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={newMemberRoles.includes(opt.code)}
|
|
||||||
onChange={() => {
|
|
||||||
setNewMemberRoles((prev) => {
|
|
||||||
const s = new Set(prev)
|
|
||||||
if (s.has(opt.code)) s.delete(opt.code)
|
|
||||||
else s.add(opt.code)
|
|
||||||
const out = Array.from(s)
|
|
||||||
return out.length ? out : ['trainer']
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{opt.label}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
|
|
||||||
<button type="button" className="btn btn-primary" style={{ flex: 1 }} onClick={submitAddClubMember}>
|
|
||||||
Hinzufügen
|
|
||||||
</button>
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => setAddMemberOpen(false)}>
|
|
||||||
Abbrechen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{clubEditModal && (
|
{clubEditModal && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -804,8 +487,7 @@ export default function AdminUsersPage() {
|
||||||
{clubEditModal.profileLabel} → {clubEditModal.clubName}
|
{clubEditModal.profileLabel} → {clubEditModal.clubName}
|
||||||
</p>
|
</p>
|
||||||
<p className="muted" style={{ fontSize: '0.82rem', lineHeight: 1.45 }}>
|
<p className="muted" style={{ fontSize: '0.82rem', lineHeight: 1.45 }}>
|
||||||
„Deaktiviert“ betrifft nur den Zugriff auf Inhalte dieses Vereins; Login und andere Vereine bleiben
|
„Deaktiviert“ betrifft nur den Zugriff auf Inhalte dieses Vereins; Login und andere Vereine bleiben unberührt.
|
||||||
unberührt.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-label">Vereinszugang</label>
|
<label className="form-label">Vereinszugang</label>
|
||||||
|
|
@ -890,15 +572,14 @@ export default function AdminUsersPage() {
|
||||||
<h2 style={{ marginTop: 0 }}>Passwort zurücksetzen</h2>
|
<h2 style={{ marginTop: 0 }}>Passwort zurücksetzen</h2>
|
||||||
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>{pwdModal.label}</p>
|
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>{pwdModal.label}</p>
|
||||||
<p className="muted" style={{ fontSize: '0.82rem', marginBottom: '0.75rem' }}>
|
<p className="muted" style={{ fontSize: '0.82rem', marginBottom: '0.75rem' }}>
|
||||||
Standard: Es wird ein sicherer Link per E-Mail verschickt (wie „Passwort vergessen“). Das bisherige
|
Standard: Es wird ein sicherer Link per E-Mail verschickt (wie „Passwort vergessen“). Das bisherige Passwort
|
||||||
Passwort bleibt gültig, bis die Person den Link nutzt und ein neues Passwort wählt.
|
bleibt gültig, bis die Person den Link nutzt und ein neues Passwort wählt.
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
<button type="button" className="btn btn-primary" onClick={submitPasswordEmail}>
|
<button type="button" className="btn btn-primary" onClick={submitPasswordEmail}>
|
||||||
Reset-Link per E-Mail senden
|
Reset-Link per E-Mail senden
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{isSuperadminViewer ? (
|
|
||||||
<>
|
<>
|
||||||
<hr style={{ margin: '1rem 0', borderColor: 'var(--border, #333)' }} />
|
<hr style={{ margin: '1rem 0', borderColor: 'var(--border, #333)' }} />
|
||||||
<p className="muted" style={{ fontSize: '0.82rem', marginBottom: '0.5rem' }}>
|
<p className="muted" style={{ fontSize: '0.82rem', marginBottom: '0.5rem' }}>
|
||||||
|
|
@ -928,7 +609,6 @@ export default function AdminUsersPage() {
|
||||||
Passwort direkt setzen
|
Passwort direkt setzen
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : null}
|
|
||||||
<div style={{ marginTop: '1rem' }}>
|
<div style={{ marginTop: '1rem' }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import api from '../utils/api'
|
||||||
import { notifyOrgInboxChanged } from '../context/OrgInboxContext'
|
import { notifyOrgInboxChanged } from '../context/OrgInboxContext'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import { activeClubMemberships } from '../utils/activeClub'
|
import { activeClubMemberships } from '../utils/activeClub'
|
||||||
|
import { isEscalatedPortalRole } from '../utils/portalRoles'
|
||||||
import PageSectionNav from '../components/PageSectionNav'
|
import PageSectionNav from '../components/PageSectionNav'
|
||||||
|
|
||||||
const CLUB_ROLE_OPTIONS = [
|
const CLUB_ROLE_OPTIONS = [
|
||||||
|
|
@ -35,6 +36,9 @@ function ClubsPage() {
|
||||||
|
|
||||||
const [editMemberModal, setEditMemberModal] = useState(null)
|
const [editMemberModal, setEditMemberModal] = useState(null)
|
||||||
const [acceptJoinModal, setAcceptJoinModal] = useState(null)
|
const [acceptJoinModal, setAcceptJoinModal] = useState(null)
|
||||||
|
const [pwdModal, setPwdModal] = useState(null)
|
||||||
|
const [pwdNew, setPwdNew] = useState('')
|
||||||
|
const [pwdNew2, setPwdNew2] = useState('')
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [formData, setFormData] = useState({})
|
const [formData, setFormData] = useState({})
|
||||||
|
|
@ -218,6 +222,43 @@ function ClubsPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const submitMembersPasswordEmail = async () => {
|
||||||
|
if (!pwdModal) return
|
||||||
|
try {
|
||||||
|
const res = await api.managementPasswordReset(pwdModal.profileId, null)
|
||||||
|
setPwdModal(null)
|
||||||
|
setPwdNew('')
|
||||||
|
setPwdNew2('')
|
||||||
|
let msg =
|
||||||
|
'Sofern eine E-Mail-Adresse hinterlegt ist, wurde ein Link zum Setzen eines neuen Passworts versendet. Das bisherige Passwort bleibt bis zur Bestätigung im Link aktiv.'
|
||||||
|
if (res?.email_sent === false) msg += ' Hinweis: E-Mail-Versand fehlgeschlagen (SMTP).'
|
||||||
|
alert(msg)
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message || String(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitMembersPasswordDirect = async () => {
|
||||||
|
if (!pwdModal || !isSuperAdmin) return
|
||||||
|
if (pwdNew.length < 8) {
|
||||||
|
alert('Mindestens 8 Zeichen.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (pwdNew !== pwdNew2) {
|
||||||
|
alert('Die beiden Passwörter stimmen nicht überein.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api.managementPasswordReset(pwdModal.profileId, pwdNew)
|
||||||
|
setPwdModal(null)
|
||||||
|
setPwdNew('')
|
||||||
|
setPwdNew2('')
|
||||||
|
alert('Neues Passwort wurde direkt gesetzt.')
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message || String(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleEdit = (item, type) => {
|
const handleEdit = (item, type) => {
|
||||||
setEditing(item)
|
setEditing(item)
|
||||||
setModalType(type)
|
setModalType(type)
|
||||||
|
|
@ -753,6 +794,34 @@ function ClubsPage() {
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => setEditMemberModal(m)}>
|
<button type="button" className="btn btn-secondary" onClick={() => setEditMemberModal(m)}>
|
||||||
Mitglied bearbeiten
|
Mitglied bearbeiten
|
||||||
</button>
|
</button>
|
||||||
|
{m.profile_id !== user?.id ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
disabled={
|
||||||
|
!isSuperAdmin &&
|
||||||
|
!isPlatformAdmin &&
|
||||||
|
isEscalatedPortalRole(m.portal_role)
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
!isSuperAdmin &&
|
||||||
|
!isPlatformAdmin &&
|
||||||
|
isEscalatedPortalRole(m.portal_role)
|
||||||
|
? 'Für Portal-Administratoren nur mit Super-/Plattform-Administrator möglich'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setPwdNew('')
|
||||||
|
setPwdNew2('')
|
||||||
|
setPwdModal({
|
||||||
|
profileId: m.profile_id,
|
||||||
|
label: m.name || m.email || `#${m.profile_id}`,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Passwort / Link
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
{m.profile_id !== user?.id ? (
|
{m.profile_id !== user?.id ? (
|
||||||
inactiveRow ? (
|
inactiveRow ? (
|
||||||
<button
|
<button
|
||||||
|
|
@ -1475,6 +1544,93 @@ function ClubsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{pwdModal ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
background: 'rgba(0,0,0,0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 1250,
|
||||||
|
padding: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'var(--surface)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '1.5rem',
|
||||||
|
maxWidth: '440px',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 style={{ marginTop: 0 }}>Passwort zurücksetzen</h2>
|
||||||
|
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>{pwdModal.label}</p>
|
||||||
|
<p className="muted" style={{ fontSize: '0.82rem', marginBottom: '0.75rem' }}>
|
||||||
|
Standard: Es wird ein sicherer Link per E-Mail verschickt (wie „Passwort vergessen“). Das bisherige Passwort
|
||||||
|
bleibt gültig, bis die Person den Link nutzt und ein neues Passwort wählt.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
|
<button type="button" className="btn btn-primary" onClick={submitMembersPasswordEmail}>
|
||||||
|
Reset-Link per E-Mail senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{isSuperAdmin ? (
|
||||||
|
<>
|
||||||
|
<hr style={{ margin: '1rem 0', borderColor: 'var(--border, #333)' }} />
|
||||||
|
<p className="muted" style={{ fontSize: '0.82rem', marginBottom: '0.5rem' }}>
|
||||||
|
Ausnahme: Passwort direkt setzen (nur bei Bedarf). Das bisherige Passwort ist danach ungültig.
|
||||||
|
</p>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Neues Passwort</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-input"
|
||||||
|
autoComplete="new-password"
|
||||||
|
value={pwdNew}
|
||||||
|
onChange={(e) => setPwdNew(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Wiederholen</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-input"
|
||||||
|
autoComplete="new-password"
|
||||||
|
value={pwdNew2}
|
||||||
|
onChange={(e) => setPwdNew2(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onClick={submitMembersPasswordDirect}
|
||||||
|
>
|
||||||
|
Passwort direkt setzen
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<div style={{ marginTop: '1rem' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onClick={() => {
|
||||||
|
setPwdModal(null)
|
||||||
|
setPwdNew('')
|
||||||
|
setPwdNew2('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -546,10 +546,8 @@ export default function MediaLibraryPage() {
|
||||||
<div className="media-library__hero-links">
|
<div className="media-library__hero-links">
|
||||||
<Link to="/">Übersicht</Link>
|
<Link to="/">Übersicht</Link>
|
||||||
<Link to="/exercises">Übungen</Link>
|
<Link to="/exercises">Übungen</Link>
|
||||||
{isPlatformAdmin ? <Link to="/admin/hierarchy">Plattform-Admin</Link> : null}
|
{isSuperadmin ? <Link to="/admin">Globale Administration</Link> : null}
|
||||||
{hasClubOrgAdmin || isPlatformAdmin ? (
|
{hasClubOrgAdmin ? <Link to="/clubs">Vereine & Mitglieder</Link> : null}
|
||||||
<Link to="/admin/users">Nutzer & Organisation</Link>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="media-library__intro">
|
<p className="media-library__intro">
|
||||||
|
|
|
||||||
5
frontend/src/utils/portalRoles.js
Normal file
5
frontend/src/utils/portalRoles.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/** Portal-/Plattformrollen auf Profil-Ebene mit besonderem Schutz (Passwort-/Rechte-Angriffsfläche). */
|
||||||
|
export function isEscalatedPortalRole(role) {
|
||||||
|
const r = (role || 'user').toLowerCase()
|
||||||
|
return r === 'admin' || r === 'superadmin'
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user