http: disable/enable user with last-admin guard + session kick

This commit is contained in:
2026-05-05 09:44:15 +01:00
parent cd3c13e2c6
commit 90bcddb27e
3 changed files with 97 additions and 0 deletions
+53
View File
@@ -270,3 +270,56 @@ func (s *Server) handleAPIUserPatch(w stdhttp.ResponseWriter, r *stdhttp.Request
})
w.WriteHeader(stdhttp.StatusOK)
}
func (s *Server) handleAPIUserDisable(w stdhttp.ResponseWriter, r *stdhttp.Request) {
actor, _ := s.requireUser(r)
id := chi.URLParam(r, "id")
u, err := s.deps.Store.GetUserByID(r.Context(), id)
if err != nil {
writeJSONError(w, stdhttp.StatusNotFound, "user_not_found", "")
return
}
if u.Role == store.RoleAdmin && u.DisabledAt == nil {
n, _ := s.deps.Store.CountEnabledAdmins(r.Context())
if n <= 1 {
writeJSONError(w, stdhttp.StatusConflict, "last_admin", "")
return
}
}
now := time.Now().UTC()
if err := s.deps.Store.DisableUser(r.Context(), id, now); err != nil {
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", err.Error())
return
}
// Kick existing sessions so the user is bounced immediately.
_, _ = s.deps.Store.DeleteSessionsByUserID(r.Context(), id)
var actorID *string
if actor != nil {
actorID = &actor.ID
}
_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{
ID: ulid.Make().String(), UserID: actorID, Actor: "user",
Action: "user.disabled", TargetKind: ptr("user"), TargetID: &id,
TS: now,
})
w.WriteHeader(stdhttp.StatusOK)
}
func (s *Server) handleAPIUserEnable(w stdhttp.ResponseWriter, r *stdhttp.Request) {
actor, _ := s.requireUser(r)
id := chi.URLParam(r, "id")
if err := s.deps.Store.EnableUser(r.Context(), id); err != nil {
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", err.Error())
return
}
var actorID *string
if actor != nil {
actorID = &actor.ID
}
_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{
ID: ulid.Make().String(), UserID: actorID, Actor: "user",
Action: "user.enabled", TargetKind: ptr("user"), TargetID: &id,
TS: time.Now().UTC(),
})
w.WriteHeader(stdhttp.StatusOK)
}