Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA. Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb, Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming, Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office (Collabora/OnlyOffice). Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort) sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
3.5 KiB
Go
116 lines
3.5 KiB
Go
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/<id>` 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)
|
||
}
|