feat(aether): initial commit – self-hosted Cloud, Phasen 1–7
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>
This commit is contained in:
commit
31a027c93a
225 files changed
+38042
No files matched your search
@@ -0,0 +1,131 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"aether/internal/auth"
|
||||
"aether/pkg/response"
|
||||
)
|
||||
|
||||
// Favoriten der Seitenleiste: anlegen, entfernen, sortieren, auf- und
|
||||
// zuklappen. Der Zustand liegt serverseitig, damit er auf jedem Gerät gleich ist.
|
||||
|
||||
// GET /api/v1/favorites
|
||||
func (h *Handler) ListFavorites(w http.ResponseWriter, r *http.Request) {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
favs, err := h.store.ListFavorites(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
if favs == nil {
|
||||
favs = []*Favorite{}
|
||||
}
|
||||
response.JSON(w, http.StatusOK, favs)
|
||||
}
|
||||
|
||||
// POST /api/v1/favorites
|
||||
func (h *Handler) AddFavorite(w http.ResponseWriter, r *http.Request) {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
var body struct {
|
||||
FolderID string `json:"folder_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.FolderID == "" {
|
||||
response.Error(w, http.StatusBadRequest, "folder_id required")
|
||||
return
|
||||
}
|
||||
folderID, err := uuid.Parse(body.FolderID)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusBadRequest, "invalid folder_id")
|
||||
return
|
||||
}
|
||||
// Lesezugriff genügt: Ein Favorit ist ein Lesezeichen, keine Änderung.
|
||||
// Die frühere Besitzprüfung schloss Gruppenordner und geteilte Ordner aus –
|
||||
// sie gehören dem Anfragenden nicht, sind aber genau die, die man sich
|
||||
// merken will.
|
||||
if _, ok := h.resolveFolder(w, r, &folderID, false); !ok {
|
||||
return
|
||||
}
|
||||
fav, err := h.store.AddFavorite(r.Context(), user.ID, folderID)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusCreated, fav)
|
||||
}
|
||||
|
||||
// DELETE /api/v1/favorites/{folderId}
|
||||
func (h *Handler) RemoveFavorite(w http.ResponseWriter, r *http.Request) {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
folderID, err := parseID(r, "folderId")
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusBadRequest, "invalid folder id")
|
||||
return
|
||||
}
|
||||
if err := h.store.RemoveFavorite(r.Context(), user.ID, folderID); errors.Is(err, ErrNotFound) {
|
||||
response.Error(w, http.StatusNotFound, "favorite not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
response.Error(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
response.NoContent(w)
|
||||
}
|
||||
|
||||
// PATCH /api/v1/favorites/{folderId}/expand
|
||||
func (h *Handler) SetFavoriteExpanded(w http.ResponseWriter, r *http.Request) {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
folderID, err := parseID(r, "folderId")
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusBadRequest, "invalid folder id")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Expanded bool `json:"expanded"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
response.Error(w, http.StatusBadRequest, "invalid body")
|
||||
return
|
||||
}
|
||||
if err := h.store.SetFavoriteExpanded(r.Context(), user.ID, folderID, body.Expanded); errors.Is(err, ErrNotFound) {
|
||||
response.Error(w, http.StatusNotFound, "favorite not found")
|
||||
return
|
||||
} else if err != nil {
|
||||
response.Error(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
response.NoContent(w)
|
||||
}
|
||||
|
||||
// PATCH /api/v1/favorites/reorder
|
||||
func (h *Handler) ReorderFavorites(w http.ResponseWriter, r *http.Request) {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
var body struct {
|
||||
FolderIDs []string `json:"folder_ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
response.Error(w, http.StatusBadRequest, "invalid body")
|
||||
return
|
||||
}
|
||||
ids := make([]uuid.UUID, 0, len(body.FolderIDs))
|
||||
for _, s := range body.FolderIDs {
|
||||
id, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusBadRequest, "invalid folder_id: "+s)
|
||||
return
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if err := h.store.ReorderFavorites(r.Context(), user.ID, ids); err != nil {
|
||||
response.Error(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
response.NoContent(w)
|
||||
}
|
||||
Reference in new issue
Block a user