{"record":{"id":"f770c83a02b2ba7d","repo":"gotify/server","slug":"cannot-delete-last-admin","errorCode":null,"errorMessage":"cannot delete last admin","messagePattern":"cannot delete last admin","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"api/user.go","lineNumber":351,"sourceCode":"//\t    schema:\n//\t        $ref: \"#/definitions/Error\"\n//\t  404:\n//\t    description: Not Found\n//\t    schema:\n//\t        $ref: \"#/definitions/Error\"\nfunc (a *UserAPI) DeleteUserByID(ctx *gin.Context) {\n\twithID(ctx, \"id\", func(id uint) {\n\t\tuser, err := a.DB.GetUserByID(id)\n\t\tif success := successOrAbort(ctx, 500, err); !success {\n\t\t\treturn\n\t\t}\n\t\tif user != nil {\n\t\t\tadminCount, err := a.DB.CountUser(&model.User{Admin: true})\n\t\t\tif success := successOrAbort(ctx, 500, err); !success {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif user.Admin && adminCount == 1 {\n\t\t\t\tctx.AbortWithError(400, errors.New(\"cannot delete last admin\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err := a.UserChangeNotifier.fireUserDeleted(id); err != nil {\n\t\t\t\tctx.AbortWithError(500, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tsuccessOrAbort(ctx, 500, a.DB.DeleteUserByID(id))\n\t\t} else {\n\t\t\tctx.AbortWithError(404, errors.New(\"user does not exist\"))\n\t\t}\n\t})\n}\n\n// ChangePassword changes the password from the current user\n// swagger:operation POST /current/user/password user updateCurrentUser\n//\n// Update the password of the current user.\n//","sourceCodeStart":333,"sourceCodeEnd":369,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/user.go#L333-L369","documentation":"Returned by DeleteUserByID with HTTP 400 when the deletion would remove the only remaining admin user. The handler counts users with Admin=true and, if the target is that last admin, refuses the delete to guarantee the system always retains at least one administrator able to manage users.","triggerScenarios":"DELETE /api/users/{id} where the target user is admin and CountUser(&User{Admin:true}) == 1; cleanup scripts deleting all demo/staging users including the bootstrap admin; tenant offboarding that removes every account in one loop.","commonSituations":"Automated teardown scripts hitting the seeded admin account; single-admin organizations running deprovisioning flows; attempting to delete an admin before promoting a replacement; migrations that strip the admin flag from everyone then try deletion.","solutions":["Promote another user to admin first (PUT /api/users/{id} with admin:true), then delete the original","Skip admin users in bulk-delete scripts (check user.Admin before DELETE)","Create a dedicated break-glass admin account so routine admins can be deleted","Re-order operations: demote the target to non-admin only after another admin exists"],"exampleFix":"// before\nawait api.deleteUserByID(adminId); // 400 cannot delete last admin\n// after\nawait api.updateUserByID(otherId, { admin: true });\nawait api.deleteUserByID(adminId);","handlingStrategy":"validation","validationCode":"const target = await api.getUserByID(id);\nif (target && target.admin) {\n  const admins = (await api.listUsers()).filter(u => u.admin);\n  if (admins.length <= 1) {\n    throw new Error('refusing to delete the last admin; promote another user first');\n  }\n}","typeGuard":"function isLastAdmin(user, allUsers) {\n  return user != null && user.admin === true && allUsers.filter(u => u.admin).length === 1;\n}","tryCatchPattern":"try {\n  await api.deleteUserByID(id);\n} catch (e) {\n  if (e.status === 400 && /last admin/.test(e.message)) { await promoteReplacementAdmin(); await api.deleteUserByID(id); }\n  else { throw e; }\n}","preventionTips":["Skip users with admin:true in bulk-delete scripts","Maintain a break-glass admin account outside automation scope","Promote-before-delete ordering in deprovisioning workflows","Monitor admin count so single-admin states are caught early"],"tags":["http","users","admin","business-rule"],"backgroundTag":"last-admin-protection","analyzedSha":"14bfc256276775c425f988d621dccfe705de18ac","analyzedAt":"2026-09-05T12:52:36.781Z","contentChangedAt":"2026-09-05T12:52:36.781Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}