BEFUND ZUERST, DENN ER WAR ANDERS ALS ERWARTET
Es gab sehr wohl eine Benachrichtigung -- ueber einen Discord-Webhook,
sauber gebaut, bewusst ohne Kundendaten im Text. Sie begann aber mit:
const url = process.env.DISCORD_WEBHOOK_WEBDESIGN;
if (!url) return;
Und diese Variable ist in .env.example nirgends aufgefuehrt. Eine
Variable, die niemand kennt, wird nicht gesetzt; dann kehrte die
Funktion wortlos zurueck. Kein Fehler, keine Protokollzeile. Es gab
eine Benachrichtigung, die nur im Quelltext existierte -- und niemand
konnte das bemerken, weil "funktioniert" und "kaputt" identisch
aussehen, solange nichts passiert.
WAS JETZT DA IST
Push aufs Handy, ohne Fremdpaket. Der interne Dienst liegt unter
/home/dogiintern mit Rechten 700 -- dort ist kein npm erreichbar, jede
Abhaengigkeit haette dauerhafte Handarbeit bedeutet. Node bringt P-256,
HKDF und AES-128-GCM selbst mit.
Die Schluessel erzeugt der Server beim ersten Start selbst und legt den
privaten Teil verschluesselt in der Datenbank ab (derselbe Weg wie die
PayPal-Zugangsdaten). Damit gibt es keinen Einrichtungsschritt, der
vergessen werden kann.
GEPRUEFT
Gegen RFC 8291 statt gegen ein Bauchgefuehl: alle fuenf Zwischenwerte
aus Anhang A stimmen (ECDH-Geheimnis, PRK_key, IKM, CEK, NONCE), und
die fertige Nachricht ist Byte fuer Byte die aus Abschnitt 5 der Norm.
Bei Kryptographie erzeugt ein Ableitungsfehler keinen Absturz, sondern
Bytes, die genauso zufaellig aussehen wie richtige.
Dazu ein Kettentest mit einem echten Empfaenger, der entschluesselt:
Kopfzeilen, Inhalt, keine Kundendaten in der Meldung, 410 loescht das
Geraet, 500 loescht es NICHT (sonst kostet eine einzelne Stoerung die
Anmeldung). 50 Pruefungen, alle gruen.
DREI ENTSCHEIDUNGEN
1. In der Meldung stehen nur Nummer und Paket. Sie erscheint auf einem
Sperrbildschirm, den auch jemand sieht, der zufaellig danebensteht.
2. Ist der Schluessel unlesbar, wird KEIN neuer erzeugt. Das waere der
bequeme Weg und der schlimmste: Ein neuer oeffentlicher Schluessel
macht schlagartig jede Anmeldung wertlos, ohne dass jemand erfaehrt,
warum nichts mehr ankommt.
3. Die Verwaltung zeigt den Zustand an und hat einen Testknopf. Genau
das fehlte dem Discord-Weg. Bei verweigerter Erlaubnis erscheint
kein Knopf, der nichts bewirkt, sondern der Weg ueber die
Browsereinstellungen.
Das stille "if (!url) return;" ist ersetzt: Jeder Weg wird einzeln
protokolliert -- auch und gerade, wenn er uebersprungen wird.
Co-Authored-By: Claude Opus 5 <[email protected]>
121 lines
4.5 KiB
JavaScript
121 lines
4.5 KiB
JavaScript
/* =====================================================================
|
|
webdesign-push.js — Geraete an- und abmelden, Stand anzeigen, testen
|
|
|
|
DER TESTKNOPF IST HIER DER WICHTIGSTE ENDPUNKT
|
|
|
|
Eine Benachrichtigung hat eine unangenehme Eigenschaft: Solange nichts
|
|
passiert, sieht "funktioniert" und "kaputt" voellig gleich aus. Genau
|
|
daran ist der bisherige Discord-Weg gescheitert -- er war eingebaut,
|
|
aber nie eingerichtet, und niemand konnte das sehen.
|
|
|
|
Deshalb gibt es hier zwei Dinge, die man sonst gern weglaesst: eine
|
|
Anzeige, die die eigentliche Frage beantwortet ("wuerde ich es
|
|
mitbekommen, wenn jetzt eine Anfrage kaeme?"), und einen Knopf, der es
|
|
ausprobiert, statt es zu behaupten.
|
|
===================================================================== */
|
|
|
|
import { json } from "../lib/http.js";
|
|
import { requireSession } from "../lib/auth.js";
|
|
import { hasPermission } from "../lib/permissions.js";
|
|
import { ausweisPruefen } from "../lib/webdesign-ausweis.js";
|
|
import { text } from "../lib/webdesign-helfer.js";
|
|
import {
|
|
geraetAnmelden, geraetAbmelden, geraeteListe, meldeAllen, stand, istBereit,
|
|
oeffentlicherSchluessel,
|
|
} from "../lib/webdesign-push.js";
|
|
|
|
/* Gleiche Struktur wie in den uebrigen Webdesign-Routen. */
|
|
async function darf(req, recht) {
|
|
const ausweis = ausweisPruefen(req);
|
|
if (ausweis) return { erlaubt: true, sitzung: { actor: ausweis.rolle, isOwner: true } };
|
|
const sitzung = await requireSession(req);
|
|
if (!sitzung) return { erlaubt: false, grund: 401, sitzung: null };
|
|
if (sitzung.isOwner) return { erlaubt: true, sitzung };
|
|
const ok = hasPermission(sitzung.permissions, sitzung.overrides, recht);
|
|
return { erlaubt: ok, grund: ok ? null : 403, sitzung };
|
|
}
|
|
|
|
/* Wer Anfragen sehen darf, darf auch ueber neue benachrichtigt werden.
|
|
Ein eigenes Recht dafuer waere eine Huerde ohne Gewinn: Die Meldung
|
|
enthaelt weniger als die Liste, die derselbe Mensch ohnehin oeffnen
|
|
darf. */
|
|
const RECHT = "WD_ANFRAGEN_VIEW";
|
|
|
|
export async function pushStand(req, res) {
|
|
const p = await darf(req, RECHT);
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
return json(res, {
|
|
ok: true,
|
|
...stand(),
|
|
geraete: geraeteListe(null),
|
|
});
|
|
}
|
|
|
|
/* Den oeffentlichen Schluessel braucht der Browser, BEVOR er sich
|
|
anmelden kann. Er ist kein Geheimnis -- er steht per Definition in
|
|
jedem Browser, der sich angemeldet hat. */
|
|
export async function pushSchluessel(req, res) {
|
|
const p = await darf(req, RECHT);
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
if (!istBereit()) {
|
|
return json(res, {
|
|
ok: false,
|
|
error: "Push ist auf dem Server nicht eingerichtet.",
|
|
}, 503);
|
|
}
|
|
return json(res, { ok: true, schluessel: oeffentlicherSchluessel() });
|
|
}
|
|
|
|
export async function pushAnmelden(req, res) {
|
|
const p = await darf(req, RECHT);
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
try {
|
|
const abo = req.body && req.body.abo;
|
|
geraetAnmelden(
|
|
abo,
|
|
p.sitzung?.userId || p.sitzung?.actor || null,
|
|
text(req.body?.bezeichnung, 80)
|
|
);
|
|
/* Sofort eine Meldung schicken. Nicht als Spielerei: Wer den Knopf
|
|
drueckt, will WISSEN, ob es geht -- und der einzige ehrliche
|
|
Beweis ist eine Meldung, die tatsaechlich auf dem Bildschirm
|
|
erscheint. */
|
|
const r = await meldeAllen({
|
|
titel: "Benachrichtigungen sind an",
|
|
text: "Ab jetzt meldet sich diese Seite bei neuen Anfragen.",
|
|
url: "/webdesign/verwaltung.html",
|
|
art: "test",
|
|
});
|
|
return json(res, { ok: true, ...stand(), probe: r });
|
|
} catch (e) {
|
|
return json(res, { ok: false, error: e.message || "Anmeldung fehlgeschlagen." }, 400);
|
|
}
|
|
}
|
|
|
|
export async function pushAbmelden(req, res) {
|
|
const p = await darf(req, RECHT);
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
const endpunkt = text(req.body?.endpunkt, 500);
|
|
if (!endpunkt) return json(res, { ok: false, error: "Keine Adresse angegeben." }, 400);
|
|
|
|
const r = geraetAbmelden(endpunkt);
|
|
return json(res, { ok: true, ...r, ...stand() });
|
|
}
|
|
|
|
export async function pushTest(req, res) {
|
|
const p = await darf(req, RECHT);
|
|
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
|
|
|
const r = await meldeAllen({
|
|
titel: "Testmeldung",
|
|
text: "Wenn du das liest, kommen Meldungen an.",
|
|
url: "/webdesign/verwaltung.html",
|
|
art: "test",
|
|
});
|
|
return json(res, { ok: true, ergebnis: r, ...stand() });
|
|
}
|