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,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
|
||||
}
|
||||
Reference in new issue
Block a user