diff --git a/IDEAS.md b/IDEAS.md index 99ad6d7..292ec2d 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -26,9 +26,11 @@ hatte notiert, das scheitere daran, dass der Browser die Sitzung nicht mitschick das Aussehen gleich mit. Deshalb liefert `/api/apps` jetzt `{apps, settings}` statt nur einer Liste. -Offen bleibt: Bei mehreren Nutzern trägt die HoneyBar die Farbe des +Offen war: Bei mehreren Nutzern trägt die HoneyBar die Farbe des Standardnutzers, weil ihr über die Site-Grenze niemand sagen kann, wer da -gerade schaut. +gerade schaut. **Für Apps unter derselben Site erledigt seit dem 29.08.2026** — +`/api/apps` sieht jetzt erst nach der Sitzung und erst dann nach dem Schlüssel. +Über eine echte Site-Grenze bleibt es, wie es war. ### 1.2 Systemmenü im Logo — ✅ gebaut (29.08.2026) @@ -153,8 +155,11 @@ Zwei Gründe, trotzdem mit der gemeinsamen anzufangen: Standardnutzers, während der Schreibtisch die eigene zeigt — derselbe Bruch wie heute bei den Farben. -→ **Zusammen mit „Persönliche Apps je Nutzer" zu bauen, und beides hängt an -derselben Bedingung: ein zweiter Nutzer oder alles unter einer Domain.** +→ **Zusammen mit „Persönliche Apps je Nutzer" zu bauen.** Die Bedingung dafür +ist am 29.08.2026 weggefallen: Aether und InkNote liegen längst unter +`*.honeyos.duckdns.org`, also unter derselben Site, und `/api/apps` sieht seither +erst nach der Sitzung. **Die HoneyBar weiß dort jetzt, wer schaut.** Was bleibt, +ist die Frage, ob es sich bei einem Nutzer lohnt. ### 1.4 HoneyOS selbst als PWA @@ -511,11 +516,11 @@ Schlüssel für die App-Liste weiß nicht, wer schaut. Die HoneyBar zeigte also gemeinsamen Apps, der Schreibtisch zusätzlich die eigenen. Zwei verschiedene Listen an zwei Stellen ist schlechter als eine. -**Liegt alles unter derselben Domain** (`os.beispiel.duckdns.org` + -`cloud.beispiel.duckdns.org`), fällt die Grenze weg: die Sitzung wird -mitgeschickt, die HoneyBar weiß, wer da ist. Seit dem Umzug auf -`os.asianxpolish.duckdns.org` ist genau das der Fall — persönliche Apps sind -damit machbar geworden. +**Liegt alles unter derselben Domain**, fällt die Grenze weg: die Sitzung wird +mitgeschickt, die HoneyBar weiß, wer da ist. **Das ist seit dem 29.08.2026 der +Fall** — Aether liegt unter `cloud.honeyos.duckdns.org`, HoneyOS unter +`honeyos.duckdns.org`, und `/api/apps` sieht erst nach der Sitzung und dann +erst nach dem Schlüssel. Persönliche Apps sind damit machbar geworden. → **Vorschlag: zurückstellen, bis es mehr als ein Konto gibt.** Bei einer Person ist „persönlich" und „für alle" dasselbe. Der Weg ist aber offen und diff --git a/README.md b/README.md index 72072f0..40dd6b2 100644 --- a/README.md +++ b/README.md @@ -90,12 +90,25 @@ Zweite Schranke, unabhängig davon: HoneyOS gibt die Liste nur an Ursprünge heraus, die **selbst als App eingetragen** sind. Eine fremde Seite scheitert also an CORS, selbst mit richtigem Schlüssel. -**Was auf zwei Sites nicht geht:** der Anmeldestand auf den Kacheln des -Schreibtischs (fragt über die Domaingrenze). Der Name *in der HoneyBar* geht -weiterhin — den holt sie bei der App selbst, gleicher Ursprung. +**Erst die Sitzung, dann der Schlüssel.** Bringt die Anfrage ein gültiges +HoneyOS-Cookie mit, antwortet der Server mit den Einstellungen **dieses** +Nutzers; der Schlüssel wird dann gar nicht erst angesehen. Er ist die Krücke +für den Fall, dass das Cookie nicht ankommen kann — und darf eine echte +Anmeldung nicht überstimmen, denn er kennt keinen Nutzer und landet beim +Standardnutzer. -Liegen alle Adressen unter derselben Domain (`os.honeyy…`, `cloud.honeyy…`), -entfällt der Schlüssel und alles funktioniert ohne Einschränkung. +Ob das Cookie ankommt, entscheidet die **Site**, nicht die Adresse: Weil +`duckdns.org` auf der Public-Suffix-Liste steht, sind `honeyos.duckdns.org` und +`cloud.honeyos.duckdns.org` **dieselbe** Site (Cookie fließt), +`honeyy.duckdns.org` dagegen eine fremde (Cookie fließt nicht). + +| | Cookie | Folge | +|---|---|---| +| App unter derselben Site | kommt an | eigene Röhre, eigenes Wetter, `data-key` überflüssig | +| App auf fremder Site | bleibt weg | Schlüssel nötig, HoneyBar trägt die Farben des Standardnutzers | + +Zweiteres gilt auch für den Anmeldestand auf den Kacheln des Schreibtischs: Der +fragt über die Domaingrenze und bleibt dort leer. ## Apps anlegen diff --git a/auth_test.go b/auth_test.go index 1e2626e..f98e4aa 100644 --- a/auth_test.go +++ b/auth_test.go @@ -483,6 +483,69 @@ func TestAppListeBrauchtDenSchlüssel(t *testing.T) { } } +// Liegt die App unter derselben Site, kommt die Sitzung mit — dann muss die +// HoneyBar die Einstellungen des Angemeldeten bekommen und nicht die des +// Standardnutzers. Der Schlüssel darf dabei ruhig mitgeschickt werden: Er ist +// die Krücke für den fremden Fall und darf die Sitzung nicht überstimmen. +func TestSitzungSchlaegtSchluessel(t *testing.T) { + idp := newFakeIdP(t, "honeyos") + s := newTestServer(t, idp.URL) + s.cfg.AppsKey = "geheim123" + s.auth = auth.New(s.cfg) + h := s.routes() + + sess, csrf := signIn(t, h) + + // Der Angemeldete stellt seine Röhre auf grün. + req := httptest.NewRequest("PUT", "/api/settings", strings.NewReader(`{"phosphor":"green"}`)) + req.Header.Set("X-CSRF-Token", csrf.Value) + req.AddCookie(sess) + req.AddCookie(csrf) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusNoContent { + t.Fatalf("Einstellungen speichern: %d", rec.Code) + } + + // Ein zweiter Nutzer mit anderer Röhre — sonst könnte der Rückfall auf den + // „einzigen Nutzer" das Ergebnis zufällig richtig aussehen lassen. + if err := s.store.SetSettings("jemand-anders", map[string]string{"phosphor": "ice"}); err != nil { + t.Fatal(err) + } + + hole := func(mod func(*http.Request)) map[string]any { + req := httptest.NewRequest("GET", "/api/apps", nil) + mod(req) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != 200 { + t.Fatalf("/api/apps: %d", rec.Code) + } + var out map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { + t.Fatal(err) + } + return out + } + + mitBeidem := hole(func(r *http.Request) { + r.AddCookie(sess) + r.Header.Set("X-HoneyOS-Key", "geheim123") + }) + if got := mitBeidem["settings"].(map[string]any)["phosphor"]; got != "green" { + t.Errorf("mit Sitzung und Schlüssel: %v, erwartet green (die des Angemeldeten)", got) + } + + // Ohne Sitzung bleibt der Schlüssel der Weg — und landet beim + // Standardnutzer. Bei zwei Nutzern heißt das: kein Rateweg, leere Menge. + nurSchluessel := hole(func(r *http.Request) { + r.Header.Set("X-HoneyOS-Key", "geheim123") + }) + if got := nurSchluessel["settings"].(map[string]any)["phosphor"]; got == "green" { + t.Errorf("nur mit Schlüssel: %v — der Schlüssel kennt keinen Nutzer und darf keinen raten", got) + } +} + // Der Schlüssel öffnet nur das Lesen der Liste, nicht das Schreiben. func TestSchlüsselDarfNichtSchreiben(t *testing.T) { idp := newFakeIdP(t, "honeyos") diff --git a/main.go b/main.go index 343ef84..c4aaa85 100644 --- a/main.go +++ b/main.go @@ -250,6 +250,22 @@ func noCache(next http.Handler) http.Handler { // damit nur für den lesbar, der ohnehin an die App herankommt. func (s *Server) appsReadable(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + // Erst die Sitzung, dann der Schlüssel. Umgekehrt wäre bequemer zu + // lesen, aber falsch: Der Schlüssel kennt keinen Nutzer und landet + // beim Standardnutzer. Liegt die App unter derselben Site wie HoneyOS + // (`cloud.honeyos.…` und `honeyos.…` sind dieselbe), schickt der + // Browser die Sitzung längst mit — dann soll die HoneyBar dort auch + // die Farben, das Wetter und später die Reihenfolge **dieses** Nutzers + // bekommen und nicht die eines fremden. + // + // Nur bei eigener Anmeldung: Ohne sie gilt jede Anfrage als angemeldet + // (offener Betrieb, Proxy davor), und das hier wäre eine offene Tür. + if s.auth.Enabled() { + if sub, ok := s.auth.SubFromRequest(r); ok { + next(w, r, sub) + return + } + } if key := s.cfg.AppsKey; key != "" { given := r.Header.Get("X-HoneyOS-Key") if given == "" {