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:
Maaxxs 2026-08-29 22:25:31 +02:00
parent 7426cb2c1f
commit 76bba33746
4 changed files with 60 additions and 30 deletions

View file

@ -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 && (

View file

@ -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

View file

@ -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>