feat: Backend mit OIDC-Anmeldung, Apps und Einstellungen im Server
Aus der statischen Seite wird ein Go-Binary mit eingebetteter Oberfläche. Grund: Apps über .env zu pflegen wurde unübersichtlich, und kosmetische Einstellungen sollen auf jedem Gerät gleich sein. - Anmeldung über den eigenen OIDC-Anbieter (PocketID), mit PKCE. Ohne Anmeldung gibt es nichts zu sehen – auch keine App-Liste. - Sitzung als signiertes Cookie ohne Gegenstück im Server; Schreibzugriffe zusätzlich über ein lesbares CSRF-Cookie abgesichert - Apps werden in der Oberfläche angelegt und bearbeitet, nicht mehr in der .env. Der Server prüft jeden Eintrag und meldet zurück, was er behält. - Einstellungen liegen je Nutzer im Server; localStorage ist nur noch der Vorschuss, damit die Röhre beim Laden nicht in der falschen Farbe blitzt - die Leiste holt die App-Liste aus /api/apps – freigegeben sind allein die Ursprünge, die selbst als App eingetragen sind - Tests fahren einen echten OIDC-Anbieter hoch und gehen den Anmeldeweg einmal ganz durch Ohne HONEYOS_OIDC_ISSUER läuft alles offen – für die Entwicklung, mit deutlicher Warnung beim Start. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
84cc5d619e
commit
283f9a1850
20 files changed
+1831
-438
No files matched your search
@@ -1,55 +1,80 @@
|
||||
# HoneyOS
|
||||
|
||||
Ein Retro-Schreibtisch vor den eigenen Apps. Amber-CRT, Boot-Log, zwei Betriebsarten —
|
||||
und dieselbe Leiste zum Einhängen in Aether und InkNote.
|
||||
Ein Retro-Schreibtisch vor den eigenen Apps. Amber-CRT, Boot-Log, zwei Betriebsarten,
|
||||
Anmeldung über den eigenen OIDC-Anbieter — und dieselbe Leiste zum Einhängen in die Apps.
|
||||
|
||||
**Was es ist:** eine Oberfläche davor. Jede App bleibt ihre eigene PWA auf ihrer eigenen
|
||||
Domain mit ihrer eigenen Arbeitsumgebung und ihrer eigenen Anmeldung. HoneyOS ist der
|
||||
Startpunkt, der Umschalter und die gemeinsame Leiste — **kein Rahmen, der die Apps
|
||||
einsperrt** (kein iframe).
|
||||
Domain mit ihrer eigenen Anmeldung. HoneyOS ist der Startpunkt, der Umschalter und die
|
||||
gemeinsame Leiste — **kein Rahmen, der die Apps einsperrt** (kein iframe).
|
||||
|
||||
**Was es nicht ist:** eine Anmeldung. HoneyOS ist eine statische Seite ohne Backend; ein
|
||||
OIDC-Client bräuchte ein Client-Geheimnis und eine Sitzung, und beides gehört nicht in
|
||||
etwas, das der Browser vollständig lesen kann. Angemeldet wird bei Aether und InkNote,
|
||||
die beide an PocketID hängen. HoneyOS kann den Stand nur **anzeigen** — siehe unten.
|
||||
**Wo der Zustand liegt:** im Server. Die App-Liste gilt für alle, die Einstellungen gehören
|
||||
je Nutzer — deshalb sieht dein HoneyOS auf jedem Gerät gleich aus, an dem du angemeldet bist.
|
||||
Der Browser hält dieselben Einstellungen nur als Vorschuss, damit die Röhre beim Laden nicht
|
||||
in der falschen Farbe aufflackert.
|
||||
|
||||
## Betrieb
|
||||
|
||||
```bash
|
||||
cp .env.example .env # Adressen und Namen eintragen
|
||||
mkdir -p /srv/honeyos && chown 1000:1000 /srv/honeyos
|
||||
cp .env.example .env # PUBLIC_URL, SESSION_SECRET, OIDC-Zugang
|
||||
docker login gitea.honeyy.duckdns.org
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
Es gibt nichts zu sichern: keine Datenbank, kein Volume. Der ganze Zustand ist die `.env` —
|
||||
und was sich der Browser pro Gerät merkt (Betriebsart, Röhrenfarbe).
|
||||
Zu sichern ist genau eine Datei: `${HONEYOS_DATA_DIR}/honeyos.json`. Darin stehen die Apps
|
||||
und die Einstellungen je Nutzer. Sichere sie zusammen mit der `.env` — ohne
|
||||
`HONEYOS_SESSION_SECRET` müssen sich alle neu anmelden, mehr passiert aber nicht.
|
||||
|
||||
Hinter deinem Reverse Proxy erwartet HoneyOS nichts Besonderes: `proxy_pass` auf den Port,
|
||||
fertig. `/healthz` antwortet mit `ok`.
|
||||
`/healthz` antwortet mit `ok`. Hinter deinem Reverse Proxy braucht es nichts Besonderes,
|
||||
außer dass `X-Forwarded-Proto: https` durchkommt — sonst setzt HoneyOS das Sitzungscookie
|
||||
ohne `Secure`.
|
||||
|
||||
## Konfiguration
|
||||
## Anmeldung
|
||||
|
||||
Alles kommt aus der `.env`. Beim Containerstart schreibt
|
||||
[`docker-entrypoint.d/20-honeyos-config.sh`](docker-entrypoint.d/20-honeyos-config.sh)
|
||||
daraus `config.json`, die der Browser liest. Nach jeder Änderung `docker compose up -d`.
|
||||
In PocketID einen Client anlegen und dort als Rückrufadresse **exakt** eintragen:
|
||||
|
||||
**Alles darin ist öffentlich.** Adressen und Namen gehören hinein, Geheimnisse niemals.
|
||||
|
||||
Eine App entsteht durch ihre `_URL`; fehlt die, wird die Nummer übersprungen (bis 12):
|
||||
|
||||
```dotenv
|
||||
HONEYOS_APP_1_ID=aether
|
||||
HONEYOS_APP_1_NAME=AETHER
|
||||
HONEYOS_APP_1_SUBTITLE=Cloud & Dateien
|
||||
HONEYOS_APP_1_URL=https://cloud.honeyy.duckdns.org
|
||||
HONEYOS_APP_1_GLYPH=cloud # cloud | note | box | chip | folder | gear
|
||||
HONEYOS_APP_1_KEY=1 # Zifferntaste im Terminal-Modus
|
||||
HONEYOS_APP_1_ME=/api/v1/auth/me # optional, siehe „Wer ist angemeldet"
|
||||
```
|
||||
https://os.honeyy.duckdns.org/api/auth/callback
|
||||
```
|
||||
|
||||
Ohne `HONEYOS_APP_1_URL` bleibt die eingebackene `web/config.json` gültig — ein
|
||||
`docker run` ohne `.env` zeigt also die Standard-Apps. Adressen lassen sich zusätzlich im
|
||||
Browser unter *Einstellungen* überschreiben; das gilt dann nur für dieses Gerät.
|
||||
HoneyOS schickt PKCE mit, braucht aber trotzdem ein Client-Geheimnis — also **keinen
|
||||
„public client"** anlegen. Danach in die `.env`:
|
||||
|
||||
```dotenv
|
||||
HONEYOS_PUBLIC_URL=https://os.honeyy.duckdns.org
|
||||
HONEYOS_SESSION_SECRET=… # openssl rand -hex 32
|
||||
HONEYOS_OIDC_ISSUER=https://id.honeyy.duckdns.org
|
||||
HONEYOS_OIDC_CLIENT_ID=honeyos
|
||||
HONEYOS_OIDC_CLIENT_SECRET=…
|
||||
HONEYOS_BUTTON_LABEL=Anmelden mit PocketID
|
||||
# HONEYOS_ALLOWED_USERS=marc # zweiter Riegel neben den PocketID-Gruppen
|
||||
```
|
||||
|
||||
Wer nicht angemeldet ist, sieht **nur** den Anmeldeschirm — keine App-Liste, keine Namen.
|
||||
Die Sitzung ist ein signiertes Cookie ohne Gegenstück auf dem Server: ein Neustart wirft
|
||||
niemanden hinaus, und es gibt nichts aufzuräumen. Sie hält 30 Tage.
|
||||
|
||||
**Ohne `HONEYOS_OIDC_ISSUER` läuft HoneyOS ganz ohne Anmeldung.** Das ist zum Entwickeln
|
||||
gedacht und sagt beim Start deutlich, dass es so nicht ins offene Netz gehört.
|
||||
|
||||
## Apps anlegen
|
||||
|
||||
**In der Oberfläche**, nicht in der `.env`: *Einstellungen → Programme* oder die Kachel
|
||||
„+ app hinzufügen". Pro App:
|
||||
|
||||
| Feld | wofür |
|
||||
|---|---|
|
||||
| Kennung | klein, ohne Leerzeichen — muss zu `data-app` der Leiste passen |
|
||||
| Name, Untertitel | Beschriftung auf Kachel und Symbol |
|
||||
| Adresse | wohin der Start springt |
|
||||
| Symbol | cloud · note · box · chip · folder · gear · globe · disk |
|
||||
| Taste | Zifferntaste im Terminal-Modus |
|
||||
| Wer-bin-ich | optional, siehe unten |
|
||||
| Token-Schlüssel | nur für Apps mit Bearer-Token im localStorage statt Cookie |
|
||||
|
||||
Gespeichert wird die Liste als Ganzes; der Server prüft sie noch einmal und antwortet mit
|
||||
dem, was er wirklich behalten hat. Eine Adresse ohne `https://` kommt als Fehlermeldung
|
||||
zurück, nicht als kaputter Eintrag.
|
||||
|
||||
## Zwei Betriebsarten, ein Datenstand
|
||||
|
||||
@@ -59,11 +84,8 @@ Browser unter *Einstellungen* überschreiben; das gilt dann nur für dieses Ger
|
||||
| Fenster | Dialog in der Mitte | verschiebbar, minimierbar |
|
||||
| unten | Statuszeile mit Tastenhinweisen | Taskleiste mit Startmenü |
|
||||
|
||||
Umschalten mit **`M`**, über *Einstellungen → Betriebsart* oder im Startmenü.
|
||||
|
||||
Gleich bleiben in beiden: die Menüleiste oben, die Palette, die Einstellungen, `config.json`.
|
||||
`renderTiles()` und `renderDeskIcons()` in `os.js` zeichnen aus denselben Daten — eine neue
|
||||
App erscheint ohne Zutun in beiden.
|
||||
Umschalten mit **`M`**, über *Einstellungen → Betriebsart* oder im Startmenü. Gleich bleiben
|
||||
in beiden: Menüleiste, Palette, Einstellungen, App-Liste.
|
||||
|
||||
**Tasten:** `1` `2` App starten · `M` Betriebsart · `S` Einstellungen · `?` Hilfe ·
|
||||
`Esc` schließt Startmenü, oberstes Fenster oder Dialog — in dieser Reihenfolge.
|
||||
@@ -77,8 +99,10 @@ Eine Zeile ins `frontend/index.html` der App:
|
||||
data-app="aether"></script>
|
||||
```
|
||||
|
||||
`data-app` ist die `id` aus der Konfiguration — danach markiert sich die Leiste selbst als
|
||||
aktiv. Die App-Liste holt sie von allein neben ihrem eigenen Skript.
|
||||
`data-app` ist die Kennung aus der App-Liste. Die Liste holt sich die Leiste selbst von
|
||||
`/api/apps` — mit deiner HoneyOS-Sitzung, weil alle Adressen unter derselben Domain liegen.
|
||||
**Freigegeben sind nur die Ursprünge, die selbst als App eingetragen sind**: die App-Liste
|
||||
ist zugleich ihre eigene CORS-Freigabe.
|
||||
|
||||
Sie rendert in einem **Shadow DOM**: Tailwind (Aether) und die InkNote-Palette kommen nicht
|
||||
hinein, ihre Stile nicht hinaus. Sie schiebt den Seiteninhalt um 32 px nach unten
|
||||
@@ -90,11 +114,11 @@ hinein, ihre Stile nicht hinaus. Sie schiebt den Seiteninhalt um 32 px nach unte
|
||||
|
||||
Vorschau ohne Einbau: `/honeybar/demo.html`.
|
||||
|
||||
## Wer ist angemeldet
|
||||
## Wer ist wo angemeldet
|
||||
|
||||
`_ME` ist der Pfad, unter dem eine App „wer bin ich" beantwortet (relativ zur `_URL` oder
|
||||
absolut). Aus der Antwort wird der Anzeigename gefischt — `display_name`, `displayName`,
|
||||
`name`, `username`, auch verschachtelt unter `user`.
|
||||
Das Feld **Wer-bin-ich** ist der Pfad, unter dem eine App „wer bin ich" beantwortet
|
||||
(relativ zur Adresse oder absolut). Aus der Antwort wird der Anzeigename gefischt —
|
||||
`display_name`, `displayName`, `name`, `username`, auch verschachtelt unter `user`.
|
||||
|
||||
Gelesen wird das an zwei Stellen, und die verhalten sich **unterschiedlich**:
|
||||
|
||||
@@ -105,38 +129,32 @@ Gelesen wird das an zwei Stellen, und die verhalten sich **unterschiedlich**:
|
||||
- InkNote: `ALLOWED_ORIGINS=https://os.honeyy.duckdns.org`
|
||||
- Aether: CORS mit `Access-Control-Allow-Credentials` — noch nicht vorhanden
|
||||
|
||||
Fehlt das, bleibt die Anzeige leer und sonst passiert nichts. Die Lampe „online"
|
||||
funktioniert unabhängig davon weiter.
|
||||
Fehlt das, bleibt die Anzeige leer und sonst passiert nichts.
|
||||
|
||||
`_TOKEN_KEY` brauchen nur Apps, die einen Bearer-Token im localStorage halten statt eines
|
||||
Cookies (InkNote: `inknote_token`). Der ist pro Domain sichtbar — von `os.…` aus also nicht
|
||||
lesbar, für die Leiste in der App dagegen schon.
|
||||
|
||||
**Voraussetzung für all das:** alle Adressen liegen unter derselben Domain
|
||||
(`*.honeyy.duckdns.org`). Sonst behandelt der Browser sie als fremde Site und schickt die
|
||||
Sitzung gar nicht erst mit.
|
||||
**Voraussetzung:** alle Adressen unter derselben Domain (`*.honeyy.duckdns.org`). Sonst
|
||||
behandelt der Browser sie als fremde Site und schickt die Sitzung gar nicht erst mit.
|
||||
|
||||
## Entwickeln
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml up -d --build # → http://localhost:8088
|
||||
go test ./... # Anmeldeablauf, CSRF, Prüfungen
|
||||
```
|
||||
|
||||
`web/` ist gemountet: Datei ändern, Seite neu laden. Weil der Mount schreibgeschützt ist,
|
||||
greift dort die `.env`-Konfiguration **nicht** — es gilt die eingebackene `web/config.json`.
|
||||
Zum Prüfen der `.env` das echte Image nehmen (`docker compose up -d --build`).
|
||||
Der Dev-Betrieb läuft **ohne Anmeldung**. Die Oberfläche steckt per `go:embed` im Binary —
|
||||
nach Änderungen an `web/` also neu bauen, ein Reload allein genügt nicht.
|
||||
|
||||
```
|
||||
web/
|
||||
index.html Desktop
|
||||
style.css alle Farben in den --p*-Variablen ganz oben
|
||||
os.js Boot, beide Ansichten, Fenster, Einstellungen
|
||||
config.json Standardkonfiguration (wird zur Laufzeit aus der .env ersetzt)
|
||||
honeybar/ die einbettbare Leiste + Vorschauseite
|
||||
looks/ zwei verworfene Entwürfe (System 7, Win95) — nur Ansicht
|
||||
main.go Konfiguration, Routen, statische Auslieferung
|
||||
auth.go OIDC mit PKCE, Sitzungscookie, CSRF
|
||||
store.go die JSON-Datei: Apps, Nutzer, Einstellungen
|
||||
auth_test.go falscher OIDC-Anbieter — der ganze Anmeldeweg im Test
|
||||
web/ index.html · style.css · os.js · honeybar/ · looks/
|
||||
```
|
||||
|
||||
Neue Symbole sind 12×12-Pixelraster in `os.js` (`GLYPHS`).
|
||||
Die Tests fahren einen echten OIDC-Anbieter mit eigenem Schlüssel hoch und gehen den Weg
|
||||
einmal ganz durch. Wenn die grün sind, liegt ein Anmeldeproblem am Anbieter oder an der
|
||||
Konfiguration, nicht am Code.
|
||||
|
||||
## Bauen und schieben
|
||||
|
||||
@@ -147,18 +165,16 @@ Neue Symbole sind 12×12-Pixelraster in `os.js` (`GLYPHS`).
|
||||
|
||||
## Grenzen, die man kennen sollte
|
||||
|
||||
- **localStorage ist pro Domain.** Betriebsart und Röhrenfarbe auf `os.…` kennt die Leiste
|
||||
auf `notes.…` nicht. Der Umschalter ist ein Schalter, kein geteilter Zustand.
|
||||
- **Die Lampe ist kein Health-Check.** Bei einer fremden Domain darf der Browser nur sehen,
|
||||
*dass* geantwortet wurde, nicht *was*. Ein 500er sähe aus wie „online".
|
||||
- **`config.json` und `honeybar.js` gehen mit `Access-Control-Allow-Origin: *` raus**, weil
|
||||
die anderen Domains sie holen. Öffentliche Konfiguration — nichts Vertrauliches hinein.
|
||||
- **Die Lampe an der Kachel ist kein Health-Check.** Bei einer fremden Domain darf der
|
||||
Browser nur sehen, *dass* geantwortet wurde. Ein 500er sähe aus wie „online".
|
||||
- **Die App-Liste ist für alle gleich.** Wer angemeldet ist, darf sie ändern — das ist für
|
||||
eine Handvoll Konten gewollt und wäre für mehr Leute zu wenig.
|
||||
- **Ein Angemeldeter kann jede Adresse eintragen.** Der Server prüft nur, dass es eine
|
||||
http(s)-Adresse ist. Das ist zugleich die CORS-Freigabe der Leiste, also nicht
|
||||
gedankenlos etwas Fremdes eintragen.
|
||||
|
||||
## Was als Nächstes ansteht
|
||||
|
||||
1. **CORS in Aether** für `os.…`, damit der Desktop den Anmeldestand sehen kann. InkNote
|
||||
hat mit `ALLOWED_ORIGINS` schon den Haken dafür.
|
||||
2. **Überall abmelden** in der Leiste. Braucht pro App den CSRF-Weg zum Logout — bei Aether
|
||||
ein `POST` mit Token, das geht nicht nebenbei über die Domaingrenze.
|
||||
3. **Fenster mit Inhalt** statt nur Verweisen: „letzte Dateien", „letzte Notizen". Fenster
|
||||
zeigen dann Daten aus den APIs — die Apps selbst bleiben in ihrem eigenen Tab.
|
||||
1. **CORS in Aether** für `os.…`, damit der Desktop den Anmeldestand sehen kann.
|
||||
2. **Überall abmelden** in der Leiste — braucht pro App den CSRF-Weg zum Logout.
|
||||
3. **Fenster mit Inhalt** statt nur Verweisen: „letzte Dateien", „letzte Notizen".
|
||||
Reference in new issue
Block a user