Mealie anbinden: Passwort einmal, danach nur noch ein Dauertoken
Unter Einstellungen gibt es jetzt eine Karte "Mealie (Rezepte)": Adresse, Benutzername, Passwort. Vorrania meldet sich damit EINMAL an (POST /api/auth/token), laesst sich von Mealie ueber POST /api/users/api-tokens ein Dauertoken ausstellen und verwirft das Passwort sofort - es steht in keiner Zeile der Datenbank, in keiner Antwort und in keinem Fehlertext. Wer sein Passwort gar nicht erst eintippen will, haekelt "Ich gebe lieber selbst ein Token ein" an. Das Token liegt NICHT in den Einstellungen: GET /settings liefert jedem angemeldeten Nutzer alle Zeilen im Klartext. Eine Ausschlussliste dort waere beim naechsten Geheimnis vergessen, deshalb eine eigene Tabelle ohne jeden Leseweg - setzen, loeschen, und die Auskunft "ist gesetzt". Was keinen Ausgang hat, kann nicht durchrutschen. Ein Test prueft genau das. Der Dienst folgt services/oidc.py, nicht off.py: Bei Open Food Facts ist "kein Treffer" der Normalfall und wird still zu None, hier ist "Mealie antwortet nicht" ein Fehler, den der Nutzer sehen muss. Die Kennungen trennen auch, was verschieden ist - ein falsches Passwort (400) ist etwas anderes als ein weggetretener Server (502). Mealie 1.x legt die Haushalts-Routen unter /api/groups/, 2.x unter /api/households/. Erkannt wird das zur Laufzeit mit einer Anfrage und dann eine Stunde gepuffert; ueber die Versionsnummer waere es unzuverlaessig, die melden Installationen uneinheitlich. Rezeptliste und -abgleich kommen als Naechstes; dies ist der Teil, mit dem sich der Server ueberhaupt erst verbinden laesst. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
37ab1ba47d
commit
7d8a54a3fc
@@ -444,6 +444,13 @@ export const api = {
|
||||
resetAll: () => request("/maintenance/reset/all", { method: "POST" }),
|
||||
|
||||
// Einstellungen
|
||||
// Mealie: Adresse und Token nimmt der Server entgegen, gibt sie aber nie
|
||||
// zurueck – die Oberflaeche erfaehrt nur „eingerichtet ja/nein".
|
||||
mealieStatus: (pruefen = false) => request(`/mealie/status${pruefen ? "?pruefen=true" : ""}`),
|
||||
mealieVerfuegbar: () => request("/mealie/available"),
|
||||
mealieKonfigurieren: (body) => request("/mealie/config", { method: "PUT", body }),
|
||||
mealieToken: (body) => request("/mealie/token", { method: "PUT", body }),
|
||||
mealieTrennen: () => request("/mealie/config", { method: "DELETE" }),
|
||||
listSettings: () => request("/settings"),
|
||||
setSetting: (key, value) =>
|
||||
request(`/settings/${key}?value=${encodeURIComponent(value)}`, { method: "PUT" }),
|
||||
|
||||
@@ -171,6 +171,7 @@ export default function Settings() {
|
||||
<button className="btn primary" disabled={busy}>{busy ? "Speichern…" : "Speichern"}</button>
|
||||
</form>
|
||||
|
||||
<MealieCard />
|
||||
<ApiTokensCard />
|
||||
<DangerZoneCard />
|
||||
</div>
|
||||
@@ -178,6 +179,143 @@ export default function Settings() {
|
||||
}
|
||||
|
||||
/** Langlebige Tokens für externe Zugriffe (Home Assistant, Skripte). */
|
||||
/**
|
||||
* Verbindung zu einer selbst gehosteten Mealie-Instanz.
|
||||
*
|
||||
* Das Passwort wird nicht gespeichert: Vorrania meldet sich damit einmal an,
|
||||
* laesst sich von Mealie ein Dauertoken ausstellen und vergisst es. Deshalb
|
||||
* wird das Feld nach dem Speichern geleert und nie wieder gefuellt – es gibt
|
||||
* nichts, was man zurueckladen koennte.
|
||||
*/
|
||||
function MealieCard() {
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const [status, setStatus] = useState(null);
|
||||
const [url, setUrl] = useState("");
|
||||
const [benutzer, setBenutzer] = useState("");
|
||||
const [passwort, setPasswort] = useState("");
|
||||
const [token, setToken] = useState("");
|
||||
const [mitToken, setMitToken] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [fehler, setFehler] = useState(null);
|
||||
|
||||
async function laden(pruefen = false) {
|
||||
try {
|
||||
const s = await api.mealieStatus(pruefen);
|
||||
setStatus(s);
|
||||
if (s.base_url) setUrl(s.base_url);
|
||||
} catch (err) { setFehler(err.message); }
|
||||
}
|
||||
useEffect(() => { laden(); }, []);
|
||||
|
||||
async function verbinden(e) {
|
||||
e.preventDefault();
|
||||
setBusy(true);
|
||||
setFehler(null);
|
||||
try {
|
||||
const s = mitToken
|
||||
? await api.mealieToken({ base_url: url, token })
|
||||
: await api.mealieKonfigurieren({ base_url: url, username: benutzer, password: passwort });
|
||||
setStatus(s);
|
||||
setPasswort(""); // bewusst sofort leeren – es ist nirgends gespeichert
|
||||
setToken("");
|
||||
toast(s.reachable ? "Mit Mealie verbunden." : "Gespeichert – Mealie antwortet aber nicht.");
|
||||
} catch (err) { setFehler(err.message); } finally { setBusy(false); }
|
||||
}
|
||||
|
||||
async function trennen() {
|
||||
const ok = await confirm({
|
||||
title: "Verbindung zu Mealie lösen?",
|
||||
message: "Adresse und Token werden hier entfernt. In Mealie selbst bleibt das "
|
||||
+ "Token bestehen – dort unter Profil ▸ API-Tokens löschbar.",
|
||||
confirmLabel: "Trennen", danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await api.mealieTrennen();
|
||||
setStatus({ configured: false, base_url: "", token_set: false });
|
||||
setUrl(""); setBenutzer(""); setPasswort(""); setToken("");
|
||||
toast("Verbindung gelöst.");
|
||||
} catch (err) { setFehler(err.message); }
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="card form-narrow" onSubmit={verbinden} style={{ marginTop: "var(--sp-4)" }}>
|
||||
<div className="card-head"><Icon name="food" /><h2>Mealie (Rezepte)</h2></div>
|
||||
<p className="muted small mt-0">
|
||||
Verbindet Vorrania mit einer selbst gehosteten Mealie-Instanz, um Rezepte
|
||||
gegen den Bestand zu prüfen. Dein Passwort wird <strong>nicht</strong>{" "}
|
||||
gespeichert: Vorrania meldet sich damit einmal an, lässt sich ein
|
||||
Dauertoken ausstellen und vergisst es.
|
||||
</p>
|
||||
|
||||
{status?.configured && (
|
||||
<div className={`alert ${status.reachable === false ? "error" : "ok"}`}>
|
||||
<Icon name={status.reachable === false ? "alert" : "check"} size={16} />
|
||||
<span>
|
||||
{status.base_url}
|
||||
{status.version ? ` · Mealie ${status.version}` : ""}
|
||||
{" · "}
|
||||
{status.reachable === false
|
||||
? (status.error || "antwortet nicht")
|
||||
: status.reachable === true ? "erreichbar" : "Token gesetzt"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{fehler && <div className="alert error"><Icon name="alert" size={16} />{fehler}</div>}
|
||||
|
||||
<label>
|
||||
Adresse
|
||||
<input value={url} placeholder="https://mealie.example.com"
|
||||
onChange={(e) => setUrl(e.target.value)} />
|
||||
</label>
|
||||
|
||||
{!mitToken ? (
|
||||
<div className="row">
|
||||
<label className="grow">
|
||||
Benutzername in Mealie
|
||||
<input value={benutzer} onChange={(e) => setBenutzer(e.target.value)} />
|
||||
</label>
|
||||
<label className="grow">
|
||||
Passwort
|
||||
<input type="password" value={passwort} autoComplete="new-password"
|
||||
onChange={(e) => setPasswort(e.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<label>
|
||||
Dauertoken aus Mealie
|
||||
<input value={token} onChange={(e) => setToken(e.target.value)}
|
||||
placeholder="in Mealie unter Profil ▸ API-Tokens erzeugen" />
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label className="check-inline">
|
||||
<input type="checkbox" checked={mitToken}
|
||||
onChange={(e) => setMitToken(e.target.checked)} />
|
||||
Ich gebe lieber selbst ein Token ein
|
||||
</label>
|
||||
|
||||
<div className="field-inline" style={{ flexWrap: "wrap", marginBottom: 0 }}>
|
||||
<button className="btn primary" disabled={busy || !url}>
|
||||
<Icon name="check" size={16} />{busy ? "Verbinde…" : "Verbinden"}
|
||||
</button>
|
||||
{status?.configured && (
|
||||
<>
|
||||
<button type="button" className="btn" onClick={() => laden(true)}>
|
||||
Verbindung prüfen
|
||||
</button>
|
||||
<button type="button" className="btn danger" onClick={trennen}>
|
||||
Trennen
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function ApiTokensCard() {
|
||||
const confirm = useConfirm();
|
||||
const [tokens, setTokens] = useState([]);
|
||||
|
||||
Reference in New Issue
Block a user