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

+63
View File
@@ -0,0 +1,63 @@
package files
import (
"errors"
"net/http"
"github.com/google/uuid"
"aether/internal/acl"
"aether/internal/auth"
"aether/pkg/response"
)
// Zugriffsprüfung für Dateien und Ordner, die dem Anfragenden nicht selbst
// gehören müssen.
//
// Die Datenbankschicht arbeitet durchgehend eigentümerbezogen (`WHERE owner_id
// = $x`) – daran ändert sich nichts. Stattdessen klären diese Helfer vorab, in
// wessen Namen die Operation läuft: bei eigenen Daten der Anfragende selbst,
// bei geliehenen der Eigentümer. Danach sieht der Rest des Handlers aus wie
// vorher.
// resolveFile liefert den Zugriffskontext für eine Datei. Ist der Zugriff nicht
// erlaubt, ist die Antwort bereits geschrieben und der zweite Rückgabewert
// false – der Handler bricht dann einfach ab.
func (h *Handler) resolveFile(w http.ResponseWriter, r *http.Request, id uuid.UUID, needWrite bool) (acl.Access, bool) {
user := auth.UserFromContext(r.Context())
if h.acl == nil {
return acl.Access{OwnerID: user.ID, OwnerUsername: user.Username, OwnerQuota: user.QuotaBytes, Permission: acl.PermWrite}, true
}
a, err := h.acl.ResolveFile(r.Context(), user.ID, id)
return finish(w, a, err, needWrite, "file")
}
// resolveFolder ist dasselbe für Ordner. folderID darf nil sein – das ist die
// eigene Wurzel, die niemandem sonst gehört.
func (h *Handler) resolveFolder(w http.ResponseWriter, r *http.Request, id *uuid.UUID, needWrite bool) (acl.Access, bool) {
user := auth.UserFromContext(r.Context())
own := acl.Access{OwnerID: user.ID, OwnerUsername: user.Username, OwnerQuota: user.QuotaBytes, Permission: acl.PermWrite}
if h.acl == nil || id == nil {
return own, true
}
a, err := h.acl.ResolveFolder(r.Context(), user.ID, *id)
return finish(w, a, err, needWrite, "folder")
}
func finish(w http.ResponseWriter, a acl.Access, err error, needWrite bool, kind string) (acl.Access, bool) {
switch {
case errors.Is(err, acl.ErrNotFound), errors.Is(err, acl.ErrNoAccess):
// Absichtlich 404 statt 403: Ein „verboten" verriete, dass es die ID
// gibt. Wer keinen Zugriff hat, soll nicht einmal das erfahren.
response.Error(w, http.StatusNotFound, kind+" not found")
return acl.Access{}, false
case err != nil:
response.Error(w, http.StatusInternalServerError, "database error")
return acl.Access{}, false
}
if needWrite && !a.CanWrite() {
response.Error(w, http.StatusForbidden, "you only have read access to this "+kind)
return acl.Access{}, false
}
return a, true
}