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

+160
View File
@@ -0,0 +1,160 @@
# Aether – Environment Variables
# Copy to .env and fill in values. Never commit .env!
# ── App ──────────────────────────────────────────────────────────────────────
APP_ENV=production # development | production
# Internal listen address. Your external reverse proxy forwards here.
APP_LISTEN_ADDR=:8080
# Public-facing URL (used for OIDC redirects, WOPI callbacks, share links).
# Must match what the browser sees — include scheme + domain, no trailing slash.
PUBLIC_URL=https://cloud.example.com
# Comma-separated IPs or CIDRs of reverse proxies allowed to set
# X-Forwarded-For / X-Forwarded-Proto. Everyone else is untrusted.
# Example: 172.18.0.1,10.0.0.0/8
TRUSTED_PROXIES=172.18.0.1
# ── Database ─────────────────────────────────────────────────────────────────
DB_HOST=postgres
DB_PORT=5432
DB_NAME=aether
DB_USER=aether
DB_PASSWORD=changeme_strong_password
# ── Session ───────────────────────────────────────────────────────────────────
# Generate with: openssl rand -hex 32
SESSION_SECRET=changeme_generate_with_openssl_rand_hex_32
SESSION_MAX_AGE=86400 # seconds (24h)
# ── Storage ───────────────────────────────────────────────────────────────────
STORAGE_PATH=/data
TMP_PATH=/data/tmp
# ── Admin seed ────────────────────────────────────────────────────────────────
# Used only on first startup to create the initial admin account.
# Remove or leave blank after first start.
ADMIN_USERNAME=admin
ADMIN_PASSWORD=changeme_strong_admin_password
# ── OIDC (optional, Phase 4) ─────────────────────────────────────────────────
# Wenn OIDC_ISSUER leer ist, ist OIDC-Login deaktiviert (kein SSO-Button).
# OIDC_REDIRECT_URL muss exakt <PUBLIC_URL>/api/v1/auth/oidc/callback sein
# und beim IdP als erlaubte Redirect-URI registriert werden.
# OIDC_ISSUER=https://your-idp.example.com
# OIDC_CLIENT_ID=aether
# OIDC_CLIENT_SECRET=
# OIDC_REDIRECT_URL=https://cloud.example.com/api/v1/auth/oidc/callback
# ── WebDAV ────────────────────────────────────────────────────────────────────
# Erreichbar unter /dav/ (Basic Auth, Username + Passwort). Kein Setting nötig.
# Reverse Proxy muss PROPFIND/MKCOL/MOVE/COPY durchreichen.
# ── Office / WOPI ─────────────────────────────────────────────────────────────
# Steht weiter unten, mit allen Adressen. (Hier stand bis zum 24.08.2026 ein
# Rest aus der Planung mit OFFICE_SERVER_URL – den Schlüssel gibt es nicht.)
# ── Features ─────────────────────────────────────────────────────────────────
# MEILISEARCH_URL=http://meilisearch:7700
# MEILISEARCH_KEY=
# ── App-Umschalter ────────────────────────────────────────────────────────────
# Andere selbst gehostete Apps, die im Menü oben in der Seitenleiste erscheinen.
# Format: Name|URL|Emoji – mehrere durch Komma getrennt. Leer = kein Menü.
# Beispiel: EXTERNAL_APPS=InkNote|https://notes.honeyy.duckdns.org|📓
EXTERNAL_APPS=
# ── OIDC: bestehendes Konto verbinden ───────────────────────────────────────
# Es gibt schon ein Konto mit demselben Benutzernamen, aber ohne Bindung an den
# Identitätsanbieter – was soll passieren?
#
# false (Voreinstellung): Es entsteht ein ZWEITES Konto. Da der Name vergeben
# ist, heißt es "name2" – leer, ohne Adminrechte, ohne deine Dateien.
# true: Das bestehende Konto wird mit der Identität verbunden. Dateien,
# Adminrecht und Freigaben bleiben; ab dann meldest du dich darüber an.
#
# Auf "true" nur, wenn du dem Anbieter glaubst, dass "marc" bei ihm derselbe
# "marc" ist wie hier – also bei einem eigenen Anbieter (PocketID, Authentik),
# dessen Konten du selbst anlegst. Bei einem Anbieter mit freier Registrierung
# wäre es die Übernahme fremder Konten.
#
# Verbunden wird nur, was noch KEINE Bindung hat – ein Konto, das schon zu einer
# anderen Identität gehört, wird nie überschrieben.
# OIDC_LINK_BY_USERNAME=true
# ── Wo die Dateien liegen ────────────────────────────────────────────────────
# Ein gewöhnliches Verzeichnis auf dem Wirt, kein Docker-Volume. Damit kommst
# du per SFTP, rsync oder im Dateimanager an deine Dateien – sie liegen dort
# als echte Ordner mit echten Namen (users/<name>/home/…).
#
# Nur lesend benutzen: Wer von Hand hineinschreibt, hat eine Datei, die die
# Cloud nicht kennt. Zum Hineinlegen ist WebDAV der Weg.
DATA_PATH=/pfad/zu/aether-data
# Der Container läuft unter dieser Kennung, damit die Dateien im Verzeichnis
# oben **dir** gehören und nicht einem Container-Nutzer. `id -u` und `id -g`
# verraten deine.
APP_UID=1000
APP_GID=1000
# Port auf dem Wirt – gebunden auf **loopback**, davor gehört dein Reverse
# Proxy. Im Container bleibt es immer 8080; hier steht nur, unter welchem Port
# der Wirt ihn anbietet.
APP_PORT=8080
# ── Volltextsuche über den Inhalt (OCR) ──────────────────────────────────────
# Holt den Text aus PDFs und Bildern, damit auch ein Scan namens
# "scan_0042.pdf" über seinen Inhalt auffindbar ist. Das Original wird dabei
# nicht verändert – der Text landet daneben in der Datenbank.
#
# Standardmäßig an. Fehlen ocrmypdf/tesseract im Image, schaltet sich der
# Arbeiter beim Start von selbst ab und die Cloud läuft ohne Volltext weiter.
# OCR_ENABLED=false
# Sprachmodelle für tesseract. Im Image liegen deu und eng; weitere brauchen
# ein zusätzliches apk-Paket (tesseract-ocr-data-<sprache>) im Dockerfile.
# OCR_LANGUAGES=deu+eng
# Höchstzahl erkannter Seiten je Datei. OCR kostet rund eine Minute je
# gescannter Seite – ohne Grenze blockiert ein dickes Handbuch die
# Warteschlange für alles andere. 0 = unbegrenzt.
# OCR_MAX_PAGES=50
# Notbremse je Datei, falls eine einzelne Seite entgleist.
# OCR_TIMEOUT=10m
# ── Office (optional) ────────────────────────────────────────────────────────
# Leer = aus. Die Anwendung ist ohne Office vollständig.
#
# Beide Anbieter sprechen WOPI, es gibt also nur einen Adapter:
# collabora – LibreOffice-Kern, ~1 GB Arbeitsspeicher, ODF nativ
# onlyoffice – beste .docx/.xlsx-Treue, 2 GB Minimum / 4 GB empfohlen
OFFICE_PROVIDER=
OFFICE_URL=
# Adresse, unter der der **Browser** den Office-Server erreicht. Leer = dieselbe
# wie OFFICE_URL. Die beiden sind selten gleich: Im Compose-Netz heisst der
# Server `collabora`, im Browser muss dort ein aufloesbarer Name stehen.
OFFICE_PUBLIC_URL=
# Adresse, unter der der **Office-Server** Aether erreicht. Leer = PUBLIC_URL.
# Der am leichtesten uebersehene Weg: Der Office-Server holt die Datei selbst
# ab, und aus seinem Container heraus ist „localhost" er selbst.
# Laufen beide im selben Compose-Projekt, ist das der Dienstname: http://app:8080
# Der veroeffentlichte Port (127.0.0.1:8090) taugt **nicht** – auf loopback
# gebunden, aus einem Container also nicht erreichbar (ECONNREFUSED).
OFFICE_WOPI_URL=
# Adressen, die das Backend anfragen darf – gegen SSRF. Der Host aus OFFICE_URL
# zählt immer dazu; hier kommt nur hinein, was zusätzlich erlaubt sein soll.
OFFICE_ALLOWED_HOSTS=
# Nur für den Testaufbau mit selbstsigniertem Zertifikat auf demselben Rechner.
OFFICE_INSECURE_TLS=false
# Für den mitgelieferten Collabora-Container (docker compose --profile office):
# unter welcher Adresse er Aether erreicht. Punkte maskieren – Collabora liest
# den Wert als regulären Ausdruck.
OFFICE_ALIASGROUP=http://app:8080
OFFICE_SSL=false
# Endet HTTPS an deinem Reverse Proxy? Produktiv true, im reinen HTTP-Test
# false – sonst verlangt Collabora vom Browser eine wss://-Verbindung, die es
# nicht gibt, und der Editor bleibt leer.
OFFICE_SSL_TERMINATION=false
OFFICE_PORT=9980