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:
HoneyandClaude Opus 5 committed 2026-08-24 09:25:36 +02:00
commit 31a027c93a
225 files changed
+38042

No files matched your search

+131
View File
@@ -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)
}