Protect the first-created admin account from being demoted or deleted
Whoever has the lowest admin_users.id is treated as the root account: its Super-Admin status can no longer be revoked and the account itself can't be deleted, regardless of how many other Super-Admins exist. UI disables the checkbox/delete button for that user with an explanation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7426cb2c1f
commit
76bba33746
4 changed files with 60 additions and 30 deletions
|
|
@ -22,6 +22,7 @@ export default function AdminUsers() {
|
|||
const [groups, setGroups] = useState([]);
|
||||
const [form, setForm] = useState(emptyForm);
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [editingIsPrimary, setEditingIsPrimary] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const load = () => {
|
||||
|
|
@ -36,6 +37,7 @@ export default function AdminUsers() {
|
|||
|
||||
const startEdit = (u) => {
|
||||
setEditingId(u.id);
|
||||
setEditingIsPrimary(u.is_primary);
|
||||
setForm({
|
||||
username: u.username,
|
||||
password: '',
|
||||
|
|
@ -47,6 +49,7 @@ export default function AdminUsers() {
|
|||
|
||||
const resetForm = () => {
|
||||
setEditingId(null);
|
||||
setEditingIsPrimary(false);
|
||||
setForm(emptyForm);
|
||||
};
|
||||
|
||||
|
|
@ -118,6 +121,7 @@ export default function AdminUsers() {
|
|||
<td className="px-4 py-2 font-medium text-neutral-800 dark:text-neutral-100">
|
||||
{u.username}
|
||||
{u.id === currentUser?.id && <span className="ml-1 text-xs text-neutral-400">(du)</span>}
|
||||
{u.is_primary && <span className="ml-1 text-xs text-neutral-400">(Erst-Administrator)</span>}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
{u.is_super_admin ? (
|
||||
|
|
@ -143,7 +147,7 @@ export default function AdminUsers() {
|
|||
</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
<button onClick={() => startEdit(u)} className="mr-3 text-brand-600 hover:underline">Bearbeiten</button>
|
||||
{u.id !== currentUser?.id && (
|
||||
{u.id !== currentUser?.id && !u.is_primary && (
|
||||
<button onClick={() => handleDelete(u.id)} className="text-red-600 hover:underline">Löschen</button>
|
||||
)}
|
||||
</td>
|
||||
|
|
@ -182,15 +186,23 @@ export default function AdminUsers() {
|
|||
</div>
|
||||
|
||||
{currentUser?.is_super_admin && (
|
||||
<label className="flex items-center gap-2 text-sm text-neutral-700 dark:text-neutral-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.is_super_admin}
|
||||
onChange={(e) => setForm({ ...form, is_super_admin: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-neutral-300 text-brand-600 focus:ring-brand-500"
|
||||
/>
|
||||
Super-Admin (hat automatisch alle Rechte)
|
||||
</label>
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm text-neutral-700 dark:text-neutral-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.is_super_admin}
|
||||
disabled={editingIsPrimary}
|
||||
onChange={(e) => setForm({ ...form, is_super_admin: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-neutral-300 text-brand-600 focus:ring-brand-500 disabled:opacity-50"
|
||||
/>
|
||||
Super-Admin (hat automatisch alle Rechte)
|
||||
</label>
|
||||
{editingIsPrimary && (
|
||||
<p className="mt-1 text-xs text-neutral-400">
|
||||
Der Erst-Administrator behält immer den Super-Admin-Status.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!form.is_super_admin && (
|
||||
|
|
|
|||
|
|
@ -23,8 +23,11 @@ class AdminUserController
|
|||
WHERE aug.admin_user_id = ?'
|
||||
);
|
||||
|
||||
$primaryId = self::primaryAdminId($db);
|
||||
|
||||
foreach ($users as &$user) {
|
||||
$user['is_super_admin'] = (bool) $user['is_super_admin'];
|
||||
$user['is_primary'] = (int) $user['id'] === $primaryId;
|
||||
$permStmt->execute([$user['id']]);
|
||||
$user['permissions'] = $permStmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||
$groupStmt->execute([$user['id']]);
|
||||
|
|
@ -94,6 +97,9 @@ class AdminUserController
|
|||
}
|
||||
|
||||
if (!$wantsSuperAdmin && AuthMiddleware::isSuperAdmin($id)) {
|
||||
if ($id === self::primaryAdminId($db)) {
|
||||
Http::error('Dem Erst-Administrator kann der Super-Admin-Status nicht entzogen werden', 422);
|
||||
}
|
||||
self::guardLastSuperAdmin($db, $id, 'Der letzte Super-Admin kann nicht degradiert werden');
|
||||
}
|
||||
|
||||
|
|
@ -126,6 +132,9 @@ class AdminUserController
|
|||
}
|
||||
|
||||
$db = Database::connection();
|
||||
if ($id === self::primaryAdminId($db)) {
|
||||
Http::error('Der Erst-Administrator kann nicht gelöscht werden', 422);
|
||||
}
|
||||
if (AuthMiddleware::isSuperAdmin($id)) {
|
||||
self::guardLastSuperAdmin($db, $id, 'Der letzte Super-Admin kann nicht gelöscht werden');
|
||||
}
|
||||
|
|
@ -135,6 +144,15 @@ class AdminUserController
|
|||
Http::send(['ok' => true]);
|
||||
}
|
||||
|
||||
// Der zuerst angelegte Account (kleinste ID) ist der Root-Administrator und darf
|
||||
// weder degradiert noch geloescht werden - unabhaengig davon, ob es noch andere
|
||||
// Super-Admins gibt. So kann man sich nie versehentlich komplett aussperren, wenn
|
||||
// z.B. alle anderen Konten geloescht werden.
|
||||
private static function primaryAdminId(\PDO $db): int
|
||||
{
|
||||
return (int) $db->query('SELECT MIN(id) FROM admin_users')->fetchColumn();
|
||||
}
|
||||
|
||||
private static function guardLastSuperAdmin(\PDO $db, int $excludingId, string $message): void
|
||||
{
|
||||
$stmt = $db->prepare('SELECT COUNT(*) FROM admin_users WHERE is_super_admin = 1 AND id != ?');
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -43,7 +43,7 @@
|
|||
"sameAs": ["https://www.youtube.com/@hifiplanet2812"]
|
||||
}
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-Ds-1nrOk.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-3HD67C-w.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/lucide-icons-66ioQXWI.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cys8aTN9.css">
|
||||
</head>
|
||||
|
|
|
|||
Loading…
Reference in a new issue