feat!: Token-Schluessel und Wer-bin-ich entfernt, Version 1.0
Beides brachte allein einen Anzeigenamen. Seit der Benutzer als Widget auf dem Schreibtisch steht, kommt der aus der HoneyOS-Anmeldung — keine App muss mehr etwas dafuer bereitstellen, und beide Leisten tragen ueberall dasselbe. Der Token-Schluessel hatte zudem eine scharfe Kante: Zeigte das Feld Wer-bin-ich auf eine fremde Adresse, ging der Ausweis der App dorthin. Weg sind damit auch checkAuth, askMe und die Anmeldezeile auf den Kacheln. Alte honeyos.json-Dateien werden weiter gelesen; die Felder verfallen beim naechsten Speichern still. 1.0, weil die Bedienung sich aendert und der Stand rund ist: zwei Leisten mit gleichem Inhalt, Widgets fuer Benutzer und Wetter, die HoneyBar auch in fremden Apps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b999a66fed
commit
fc4dd12c5e
7 files changed
+22
-168
No files matched your search
@@ -131,7 +131,6 @@ Solange das so bleibt, kann HoneyOS **keine InkNote-Daten anzeigen**. Der Weg
|
||||
wäre, dass InkNote seine Sitzung zusätzlich als `HttpOnly`-Cookie setzt; der
|
||||
Token-Weg kann daneben bleiben, damit PWA und Offline-Betrieb unangetastet sind.
|
||||
|
||||
Erledigt zugleich den Token-Schlüssel (siehe „Offene Entscheidungen").
|
||||
|
||||
### 1.5b HoneyOS selbst als PWA
|
||||
|
||||
@@ -356,23 +355,16 @@ Aether oder InkNote neu zu bauen.
|
||||
→ **Erst in einer App einbauen, eine Woche benutzen, dann entscheiden.** Nicht
|
||||
in beide gleichzeitig.
|
||||
|
||||
### Token-Schlüssel behalten oder löschen?
|
||||
### Token-Schlüssel behalten oder löschen? — ✅ gelöscht (29.08.2026)
|
||||
|
||||
**Empfehlung: löschen.** Gründe:
|
||||
Mitsamt dem Feld „Wer-bin-ich". Die Begründung von damals hat sich gehalten: Es
|
||||
brachte allein den Anzeigenamen, wirkte an zwei Stellen unterschiedlich, und ein
|
||||
Token an eine fremde Adresse zu schicken war eine Falle, die niemand braucht.
|
||||
|
||||
1. Er wirkt nur an einer von zwei Stellen — in der HoneyBar ja, auf dem Desktop
|
||||
nie. Das ist schwer zu erklären und noch schwerer zu merken.
|
||||
2. Er bringt allein die Namensanzeige. Kein Funktionsunterschied.
|
||||
3. Sobald 1.3 gebaut ist, ist er ohnehin überflüssig.
|
||||
4. **Scharfe Kante:** Das Feld „Wer-bin-ich" darf auch eine vollständige fremde
|
||||
Adresse sein. Steht dort etwas Fremdes *und* ein Token-Schlüssel, schickt die
|
||||
HoneyBar den InkNote-Token dorthin. Das setzt voraus, dass jemand Angemeldetes
|
||||
das selbst einträgt — bei einer Ein-Personen-Instanz also kein akuter
|
||||
Notstand, aber eine Falle, die niemand braucht.
|
||||
Den Namen zeigt jetzt das Benutzer-Widget auf dem Schreibtisch, aus der
|
||||
HoneyOS-Anmeldung — dort, wo er hingehört, und ohne dass eine App etwas dafür
|
||||
tun muss.
|
||||
|
||||
Wird er doch behalten, gehört mindestens eingebaut: **mit Token-Schlüssel nur
|
||||
relative Pfade zulassen**, damit ein Ausweis nie an einen fremden Ursprung geht.
|
||||
Drei Zeilen.
|
||||
|
||||
### Persönliche Apps je Nutzer?
|
||||
|
||||
|
||||
@@ -109,8 +109,6 @@ entfällt der Schlüssel und alles funktioniert ohne Einschränkung.
|
||||
| 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
|
||||
@@ -388,25 +386,21 @@ nur, wessen App selbst eine schickt.
|
||||
- **Andere Proxys:** Caddy braucht ein Zusatzmodul (`replace-response`),
|
||||
Traefik ein Plugin für den Antwortkörper. nginx kann es ohne Erweiterung.
|
||||
|
||||
## Wer ist wo angemeldet
|
||||
## Wer angemeldet ist
|
||||
|
||||
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`.
|
||||
Steht auf dem Schreibtisch, als eigenes Widget: Bild, Name und Begrüßung, aus
|
||||
der HoneyOS-Anmeldung. In den Apps steht es **nicht** — die HoneyBar dort trägt
|
||||
genau dasselbe wie die Menüleiste hier, sonst merkt man beim Wechsel, dass es
|
||||
zwei verschiedene Leisten sind.
|
||||
|
||||
Gelesen wird das an zwei Stellen, und die verhalten sich **unterschiedlich**:
|
||||
Früher konnte jede App einen „Wer-bin-ich"-Pfad und einen Token-Schlüssel
|
||||
mitbringen; die HoneyBar fragte damit die App nach dem Anzeigenamen. Beides ist
|
||||
seit dem 29.08.2026 **entfernt**. Es brachte allein einen Namen, wirkte je nach
|
||||
Domain unterschiedlich, und der Token-Schlüssel hatte eine scharfe Kante: Zeigte
|
||||
das Wer-bin-ich-Feld auf eine fremde Adresse, ging der Ausweis der App dorthin.
|
||||
Wer die Felder in seiner `honeyos.json` noch stehen hat, braucht nichts zu tun —
|
||||
sie werden beim nächsten Speichern still verworfen.
|
||||
|
||||
1. **Die HoneyBar in der App** fragt ihre eigene App. Gleicher Ursprung → funktioniert
|
||||
sofort, ohne Änderung an Aether oder InkNote.
|
||||
2. **Der Desktop auf `os.…`** fragt über die Domaingrenze. Dafür muss die App diesen
|
||||
Ursprung erlauben:
|
||||
- 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.
|
||||
|
||||
**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
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const localSub = "local"
|
||||
// Version ist die einzige Stelle, an der sie steht. build.sh liest sie hier aus
|
||||
// und vergibt denselben Docker-Tag; die Oberfläche holt sie über /api/public.
|
||||
// Vorher stand sie zusätzlich in os.js und lief mit den Image-Tags auseinander.
|
||||
const Version = "0.9"
|
||||
const Version = "1.0"
|
||||
|
||||
type Config struct {
|
||||
Addr string
|
||||
|
||||
@@ -24,8 +24,6 @@ type App struct {
|
||||
URL string `json:"url"`
|
||||
Glyph string `json:"glyph,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Me string `json:"me,omitempty"`
|
||||
TokenKey string `json:"tokenKey,omitempty"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
@@ -117,8 +115,6 @@ func (s *Store) SetApps(apps []App) error {
|
||||
a.URL = strings.TrimSpace(a.URL)
|
||||
a.Glyph = strings.TrimSpace(a.Glyph)
|
||||
a.Key = strings.TrimSpace(a.Key)
|
||||
a.Me = strings.TrimSpace(a.Me)
|
||||
a.TokenKey = strings.TrimSpace(a.TokenKey)
|
||||
|
||||
if !idPattern.MatchString(a.ID) {
|
||||
return fmt.Errorf("App %d: ID muss klein geschrieben sein und mit Buchstabe oder Ziffer beginnen", i+1)
|
||||
@@ -141,7 +137,7 @@ func (s *Store) SetApps(apps []App) error {
|
||||
if len([]rune(a.Key)) > 1 {
|
||||
return fmt.Errorf("App %q: Taste ist ein einzelnes Zeichen", a.ID)
|
||||
}
|
||||
for _, f := range []string{a.Name, a.Subtitle, a.URL, a.Me, a.TokenKey} {
|
||||
for _, f := range []string{a.Name, a.Subtitle, a.URL} {
|
||||
if len(f) > maxFieldLen {
|
||||
return fmt.Errorf("App %q: ein Feld ist länger als %d Zeichen", a.ID, maxFieldLen)
|
||||
}
|
||||
|
||||
@@ -153,8 +153,6 @@
|
||||
background: var(--p); border-color: var(--p); color: var(--bg); text-shadow: none;
|
||||
}
|
||||
.right { display: flex; align-items: center; gap: 8px; }
|
||||
.user { color: var(--p); max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.user[hidden] { display: none; }
|
||||
.clock { color: var(--p-mid); font-variant-numeric: tabular-nums; }
|
||||
/* Anzeige, kein Knopf: Wer darauf klickt, erwartet, dass etwas passiert.
|
||||
Eingestellt wird das Wetter dort, wo alles eingestellt wird. */
|
||||
@@ -187,7 +185,6 @@
|
||||
<a class="home" id="home" title="HoneyOS">■ HONEY<b>OS</b></a>
|
||||
<nav class="apps" id="apps"></nav>
|
||||
<div class="right">
|
||||
<span class="user" id="user" hidden></span>
|
||||
<span class="wx" id="wx" hidden></span>
|
||||
<span class="clock" id="clock"></span>
|
||||
<button class="menu-btn" id="menuBtn" aria-haspopup="true" aria-expanded="false">▾</button>
|
||||
@@ -279,41 +276,6 @@
|
||||
el.hidden = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt an, wer angemeldet ist. Die HoneyBar steckt in der App selbst, der
|
||||
* Aufruf geht also an denselben Ursprung — kein CORS, keine Freigabe nötig.
|
||||
* Antwortet die App nicht wie erwartet, bleibt das Feld leer und niemand
|
||||
* merkt etwas: die Anzeige ist eine Zugabe, keine Zusicherung.
|
||||
*/
|
||||
async function showUser() {
|
||||
const me = apps.find((a) => a.id === current);
|
||||
if (!me || !me.me) return;
|
||||
try {
|
||||
const headers = {};
|
||||
if (me.tokenKey) {
|
||||
const tok = localStorage.getItem(me.tokenKey);
|
||||
if (tok) headers.Authorization = "Bearer " + tok;
|
||||
}
|
||||
const res = await fetch(url(me, me.me), { credentials: "same-origin", cache: "no-store", headers });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const u = (data && (data.user || data.profile)) || data || {};
|
||||
const name = u.display_name || u.displayName || u.name || u.username || u.preferred_username;
|
||||
if (!name) return;
|
||||
const el = root.getElementById("user");
|
||||
el.textContent = name;
|
||||
el.hidden = false;
|
||||
} catch {
|
||||
/* nicht angemeldet, kein Endpunkt, kein Netz — alles kein Grund zu lärmen */
|
||||
}
|
||||
}
|
||||
|
||||
function url(app, path) {
|
||||
if (!path) return app.url;
|
||||
if (/^https?:\/\//i.test(path)) return path;
|
||||
return app.url.replace(/\/+$/, "") + (path.startsWith("/") ? path : "/" + path);
|
||||
}
|
||||
|
||||
function clock() {
|
||||
root.getElementById("clock").textContent = new Date().toLocaleTimeString("de-DE", {
|
||||
hour: "2-digit",
|
||||
@@ -427,7 +389,6 @@
|
||||
nub.remove();
|
||||
nub = null;
|
||||
mount();
|
||||
showUser();
|
||||
});
|
||||
document.body.appendChild(nub);
|
||||
}
|
||||
@@ -492,7 +453,6 @@
|
||||
if (document.body) mount();
|
||||
else document.addEventListener("DOMContentLoaded", mount, { once: true });
|
||||
|
||||
showUser();
|
||||
wetter();
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ const state = {
|
||||
oidc: false,
|
||||
settings: { ...DEFAULT_SETTINGS },
|
||||
probes: {}, // appId -> "up" | "down" | "unknown"
|
||||
auth: {}, // appId -> {status: "in"|"out"|"blind", name}
|
||||
wins: {}, // id -> {el, task, min}
|
||||
zTop: 10,
|
||||
};
|
||||
@@ -650,7 +649,6 @@ async function probeApps() {
|
||||
);
|
||||
paintProbes();
|
||||
renderStats();
|
||||
checkAuth();
|
||||
}
|
||||
|
||||
async function probe(url) {
|
||||
@@ -666,91 +664,11 @@ async function probe(url) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Anmeldestand ──────────────────────────────────────────────────────────
|
||||
* Wenn eine App einen "me"-Endpunkt in der Konfiguration hat, fragen wir ihn
|
||||
* mit den Keksen des Browsers. Das funktioniert nur, weil alle Adressen unter
|
||||
* derselben Domain liegen — sonst schickt der Browser die Sitzung nicht mit.
|
||||
*
|
||||
* Antwortet die App nicht oder verbietet CORS, ist der Stand "blind": dann
|
||||
* zeigt HoneyOS eben nur die Lampe wie bisher. Kein Fehler, keine Meldung —
|
||||
* die Anzeige ist eine Zugabe, nichts, worauf sich jemand verlassen muss.
|
||||
*/
|
||||
async function checkAuth() {
|
||||
await Promise.all(
|
||||
state.cfg.apps
|
||||
.filter((a) => a.me)
|
||||
.map(async (app) => {
|
||||
state.auth[app.id] = await askMe(app);
|
||||
})
|
||||
);
|
||||
paintProbes();
|
||||
applyAuthName();
|
||||
}
|
||||
|
||||
async function askMe(app) {
|
||||
const ctl = new AbortController();
|
||||
const t = setTimeout(() => ctl.abort(), 5000);
|
||||
try {
|
||||
const headers = {};
|
||||
// Apps mit Bearer-Token statt Cookie: der Schlüssel steht in der
|
||||
// Konfiguration, der Wert im localStorage — und der ist pro Domain.
|
||||
// Von hier aus lesbar ist er nur, wenn HoneyOS dieselbe Domain ist.
|
||||
if (app.tokenKey) {
|
||||
try {
|
||||
const tok = localStorage.getItem(app.tokenKey);
|
||||
if (tok) headers.Authorization = `Bearer ${tok}`;
|
||||
} catch {
|
||||
/* kein Speicherzugriff — dann ohne */
|
||||
}
|
||||
}
|
||||
const res = await fetch(resolveUrl(app, app.me), {
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
headers,
|
||||
signal: ctl.signal,
|
||||
});
|
||||
if (res.status === 401 || res.status === 403) return { status: "out" };
|
||||
if (!res.ok) return { status: "blind" };
|
||||
return { status: "in", name: extractName(await res.json()) };
|
||||
} catch {
|
||||
return { status: "blind" };
|
||||
} finally {
|
||||
clearTimeout(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Relative Pfade beziehen sich auf die App-Adresse, absolute gelten wie sie sind. */
|
||||
function resolveUrl(app, path) {
|
||||
if (!path) return app.url;
|
||||
if (/^https?:\/\//i.test(path)) return path;
|
||||
return app.url.replace(/\/+$/, "") + (path.startsWith("/") ? path : "/" + path);
|
||||
}
|
||||
|
||||
/** Jede App antwortet anders — hier wird der Anzeigename herausgefischt. */
|
||||
function extractName(data) {
|
||||
const u = (data && (data.user || data.profile)) || data || {};
|
||||
return u.display_name || u.displayName || u.name || u.username || u.preferred_username || "";
|
||||
}
|
||||
|
||||
/** Nennt eine App einen Namen, gilt der — er kommt vom Anbieter, die
|
||||
* Einstellung ist nur der Platzhalter, solange niemand antwortet. */
|
||||
function applyAuthName() {
|
||||
const named = Object.values(state.auth).find((a) => a.status === "in" && a.name);
|
||||
if (named) {
|
||||
state.cfg.user = named.name;
|
||||
renderUser();
|
||||
renderStats();
|
||||
}
|
||||
}
|
||||
|
||||
const STATE_TEXT = { up: "online", down: "keine antwort", unknown: "prüfe …" };
|
||||
const AUTH_TEXT = { in: "angemeldet", out: "nicht angemeldet" };
|
||||
|
||||
function stateLine(appId) {
|
||||
const s = state.probes[appId] || "unknown";
|
||||
const a = state.auth[appId];
|
||||
const extra = a && AUTH_TEXT[a.status] ? ` · ${a.status === "in" && a.name ? a.name : AUTH_TEXT[a.status]}` : "";
|
||||
return { s, text: STATE_TEXT[s] + (s === "up" ? extra : "") };
|
||||
return { s, text: STATE_TEXT[s] };
|
||||
}
|
||||
|
||||
function paintProbes() {
|
||||
@@ -1211,12 +1129,6 @@ function openApps() {
|
||||
</select></label>
|
||||
<label class="ae-field"><span>Taste</span>
|
||||
<input data-i="${i}" data-f="key" value="${escapeAttr(a.key || "")}" maxlength="1" placeholder="1" /></label>
|
||||
<label class="ae-field"><span>Wer-bin-ich</span>
|
||||
<input data-i="${i}" data-f="me" value="${escapeAttr(a.me || "")}" placeholder="/api/v1/auth/me" spellcheck="false" /></label>
|
||||
<p class="ae-hint">optional — zeigt an, ob du dort angemeldet bist</p>
|
||||
<label class="ae-field"><span>Token-Schlüssel</span>
|
||||
<input data-i="${i}" data-f="tokenKey" value="${escapeAttr(a.tokenKey || "")}" placeholder="leer lassen" spellcheck="false" /></label>
|
||||
<p class="ae-hint">nur für Apps mit Bearer-Token im localStorage statt Cookie</p>
|
||||
<div class="ae-actions">
|
||||
<button class="btn btn-danger" data-del="${i}">löschen</button>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user