feat(web): Herkunft der Fehlermeldung — Cookie tessera_desktop_client und Client-Felder in der Nutzlast

Die Middleware liest jetzt zusaetzlich dv/dc/dos aus der Anfrage und legt
daraus das Cookie tessera_desktop_client an (bereinigt per Muster, nur
wenn alle drei Werte gueltig sind); desktop-client.ts liest es zurueck.
Der Fehler-melden-Dialog fuellt daraus vier neue Nutzlastfelder
(clientKind/clientOs/clientVersion/clientCommit), damit die API die
Herkunft der Meldung ausweisen kann. Ohne das zweite Cookie (alter
Client) bleibt es bei "Desktop-App (unbekannt)".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g2npLxzH5gZpg8s2S6vKh
This commit is contained in:
2026-09-18 12:36:19 +02:00
parent b03cb211b3
commit f2457113a2
8 changed files with 376 additions and 10 deletions
+52 -4
View File
@@ -14,6 +14,46 @@ const publicRoutes = ['/login', '/reset-password'];
const DESKTOP_COOKIE = 'tessera_desktop';
/**
* Zweites Cookie (quick-260918-gza): traegt Version, Commit-Stempel und
* Betriebssystem der Desktop-App, aus denen der Fehler-melden-Knopf die
* Herkunft einer Meldung fuellt (Betreff-Kuerzel `[Desktop/Windows]` bzw.
* `[Desktop/Linux]` statt `[Browser]` -- WebView2 und WebKitGTK sehen im
* User-Agent sonst wie ein gewoehnlicher Browser aus). `httpOnly: false`
* ist Absicht: `getDesktopClientInfo()` (apps/web/src/lib/desktop-client.ts)
* liest es aus Seiten-JavaScript, Version/Betriebssystem sind kein Geheimnis
* -- dieselbe Vertrauensstufe wie der User-Agent, den jede Seite ohnehin
* liest. Next.js serialisiert den Wert mit `encodeURIComponent`
* (`|` wird `%7C`, gemessen zur Planungszeit).
*/
const DESKTOP_CLIENT_COOKIE = 'tessera_desktop_client';
/** Nur `[A-Za-z0-9.+_-]`, hoechstens 40 Zeichen, mindestens 1 Zeichen. */
const DESKTOP_VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,39}$/;
/** Leer erlaubt (kein Commit-Stempel im lokalen Bau), sonst hoechstens 40 Hex-/Alnum-Zeichen. */
const DESKTOP_COMMIT_RE = /^[A-Za-z0-9]{0,40}$/;
/** `std::env::consts::OS`-Werte sind kleingeschrieben ASCII, hoechstens 20 Zeichen. */
const DESKTOP_OS_RE = /^[a-z]{1,20}$/;
/**
* Baut den Wert fuer `tessera_desktop_client` aus den Query-Parametern
* `dv`/`dc`/`dos` (T-GZA-03: feste Muster, Gesamtlaenge begrenzt). Fehlt
* einer der drei Parameter oder passt er nicht auf sein Muster, liefert
* die Funktion `null` -- die Middleware setzt das Cookie dann NICHT (ein
* alter Desktop-Client ohne diese Parameter bleibt ueber `tessera_desktop`
* als Desktop-App erkennbar, nur ohne Details).
*/
function buildDesktopClientCookieValue(params: URLSearchParams): string | null {
const dv = params.get('dv');
const dc = params.get('dc');
const dos = params.get('dos');
if (dv === null || dc === null || dos === null) return null;
if (!DESKTOP_VERSION_RE.test(dv) || !DESKTOP_COMMIT_RE.test(dc) || !DESKTOP_OS_RE.test(dos)) {
return null;
}
return `${dv}|${dc}|${dos}`;
}
/**
* Setzt das Cookie `tessera_desktop`, wenn die Anfrage `?desktop=1` traegt
* (260917-h2s). Der Desktop-Client (apps/desktop/src-tauri/src/lib.rs,
@@ -22,17 +62,25 @@ const DESKTOP_COOKIE = 'tessera_desktop';
* Fruehausstieg fuer oeffentliche Routen und auf Redirects -- sonst geht die
* Kennung beim 307 nach /login verloren. `httpOnly: false` ist Absicht (wird
* von `isDesktopClient()` in apps/web/src/lib/desktop-client.ts gelesen);
* der Wert ist kein Geheimnis.
* der Wert ist kein Geheimnis. Seit quick-260918-gza setzt dieselbe Funktion
* zusaetzlich das Cookie `tessera_desktop_client` (Version/Commit/OS), sofern
* die Anfrage gueltige Werte fuer `dv`/`dc`/`dos` mitbringt -- ohne sie wird
* das zweite Cookie weder gesetzt noch geloescht.
*/
function withDesktopCookie(req: NextRequest, res: NextResponse): NextResponse {
if (req.nextUrl.searchParams.get('desktop') === '1') {
res.cookies.set(DESKTOP_COOKIE, '1', {
const cookieOptions = {
path: '/',
maxAge: 60 * 60 * 24 * 365,
sameSite: 'lax',
sameSite: 'lax' as const,
httpOnly: false,
secure: req.nextUrl.protocol === 'https:',
});
};
res.cookies.set(DESKTOP_COOKIE, '1', cookieOptions);
const info = buildDesktopClientCookieValue(req.nextUrl.searchParams);
if (info !== null) {
res.cookies.set(DESKTOP_CLIENT_COOKIE, info, cookieOptions);
}
}
return res;
}