package admin import ( "encoding/json" "errors" "net/http" "strings" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/rs/zerolog/log" "aether/internal/audit" "aether/internal/files" "aether/pkg/response" ) // Gruppenordner werden ausschließlich hier verwaltet. Bewusst nicht in der // Dateiansicht: Ein Gruppenordner gehört keinem der Mitglieder, also soll ihn // auch keines von ihnen entfernen können. // GET /api/v1/admin/group-folders func (h *Handler) ListGroupFolders(w http.ResponseWriter, r *http.Request) { if _, ok := requireAdmin(r); !ok { response.Error(w, http.StatusForbidden, "admin required") return } list, err := h.files.ListGroupFolders(r.Context()) if err != nil { response.Error(w, http.StatusInternalServerError, "could not load group folders") return } response.JSON(w, http.StatusOK, list) } // POST /api/v1/admin/group-folders func (h *Handler) CreateGroupFolder(w http.ResponseWriter, r *http.Request) { if _, ok := requireAdmin(r); !ok { response.Error(w, http.StatusForbidden, "admin required") return } var body struct { GroupID string `json:"group_id"` Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { response.Error(w, http.StatusBadRequest, "invalid body") return } groupID, err := uuid.Parse(body.GroupID) if err != nil { response.Error(w, http.StatusBadRequest, "invalid group_id") return } // Gleiche Namensregeln wie überall – der Name wird Teil des Ablagepfads. name := files.SanitizeFilename(strings.TrimSpace(body.Name)) if name == "" { response.Error(w, http.StatusBadRequest, "name required") return } folder, err := h.files.CreateGroupFolder(r.Context(), groupID, name) if errors.Is(err, files.ErrGroupFolderExists) { response.Error(w, http.StatusConflict, "this group already has a folder") return } if errors.Is(err, files.ErrNameTaken) { response.Error(w, http.StatusConflict, "a folder with that name already exists") return } if err != nil { response.Error(w, http.StatusInternalServerError, "could not create group folder") return } h.logAdmin(r, audit.ActionGroupFolderCreate, "folder", folder.ID.String(), folder.Name, "group "+groupID.String()) response.JSON(w, http.StatusCreated, folder) } // DELETE /api/v1/admin/group-folders/{id} func (h *Handler) DeleteGroupFolder(w http.ResponseWriter, r *http.Request) { if _, ok := requireAdmin(r); !ok { response.Error(w, http.StatusForbidden, "admin required") return } id, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { response.Error(w, http.StatusBadRequest, "invalid folder id") return } groupID, err := h.files.DeleteGroupFolderTree(r.Context(), id) if errors.Is(err, files.ErrNotFound) { response.Error(w, http.StatusNotFound, "group folder not found") return } if err != nil { response.Error(w, http.StatusInternalServerError, "could not delete group folder") return } // Speicher erst nach dem Datenbank-Commit, und am Stück statt Blob für // Blob: Da eine Gruppe genau einen Ordner hat, ist `groups/` exakt ihr // Bereich – inklusive Papierkorb und Versionen. Einzeln gelöscht blieben // leere Verzeichnisse zurück. if err := h.storage.DeletePrefix(r.Context(), "groups/"+groupID.String()); err != nil { log.Ctx(r.Context()).Warn().Err(err).Str("group_id", groupID.String()). Msg("group folder deleted but storage remains") } h.logAdmin(r, audit.ActionGroupFolderDelete, "folder", id.String(), "", "group "+groupID.String()+" and all its files") w.WriteHeader(http.StatusNoContent) }