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
@@ -8,14 +8,19 @@ import { computeCaptureSize } from '@/lib/bug-report-api';
/**
* bug-report-button.test — NEU (quick-260914-m97, Fehler-melden-Knopf).
*
* Elf Tests. Die Kernanforderung (Test 1): das Bild wird VOR dem Dialog
* aufgenommen — der `toPng`-Mock prueft waehrend seines Aufrufs, dass noch
* KEIN `role="dialog"` im DOM steht. `html-to-image` selbst laeuft in jsdom
* nicht (gemessen zur Planungszeit: `HTMLVideoElement is not defined`, kein
* Canvas-Backend) — deshalb der Mock; der Bildbeweis kommt aus dem Browser.
* Dreizehn Tests. Die Kernanforderung (Test 1): das Bild wird VOR dem
* Dialog aufgenommen — der `toPng`-Mock prueft waehrend seines Aufrufs,
* dass noch KEIN `role="dialog"` im DOM steht. `html-to-image` selbst
* laeuft in jsdom nicht (gemessen zur Planungszeit:
* `HTMLVideoElement is not defined`, kein Canvas-Backend) — deshalb der
* Mock; der Bildbeweis kommt aus dem Browser.
*
* Der next-intl-Mock liest die Texte aus der ECHTEN `de.json` (Muster
* `tessera-logo.test.tsx`), Erwartungen zitieren `de.bugReport.<key>`.
*
* Test 12/13 (quick-260918-gza): die vier Herkunftsfelder in der Nutzlast,
* abhaengig vom Cookie `tessera_desktop_client` — jsdom hat ohne gesetztes
* Cookie keinen Desktop-Client, darum bleibt Test 2 beim Browser-Fall.
*/
const { mockToPng, mockFetch, mockUser } = vi.hoisted(() => ({
mockToPng: vi.fn(),
@@ -86,9 +91,15 @@ beforeEach(() => {
vi.stubGlobal('fetch', mockFetch);
});
function clearDesktopCookies() {
document.cookie = 'tessera_desktop=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
document.cookie = 'tessera_desktop_client=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
}
afterEach(() => {
cleanup();
clearErrorBuffer();
clearDesktopCookies();
vi.unstubAllGlobals();
});
@@ -142,6 +153,10 @@ describe('BugReportButton (quick-260914-m97)', () => {
expect(body.get('userAgent')).toBeTruthy();
expect(body.get('viewport')).toMatch(/^\d+x\d+$/);
expect(body.get('clientTime')).toMatch(/^\d{4}-\d{2}-\d{2}T/);
expect(body.get('clientKind')).toBe('browser');
expect(body.get('clientOs')).toBe('');
expect(body.get('clientVersion')).toBe('');
expect(body.get('clientCommit')).toBe('');
expect(body.getAll('errors').some((e) => String(e).includes('GET /modules -> 500'))).toBe(true);
const shot = body.get('screenshot');
expect(shot).toBeInstanceOf(Blob);
@@ -271,6 +286,41 @@ describe('BugReportButton (quick-260914-m97)', () => {
expect(screen.queryByText(specific)).toBeNull();
}
});
it('Test 12 (quick-260918-gza): Desktop-Cookies gesetzt -> clientKind desktop mit Os/Version/Commit aus dem Cookie', async () => {
document.cookie = 'tessera_desktop=1; path=/';
document.cookie = 'tessera_desktop_client=1.2.0%7Ca6d1a64%7Cwindows; path=/';
mockFetch.mockResolvedValue(jsonResponse(200, { sent: true }));
const user = userEvent.setup();
await renderButton();
await openDialog(user);
await user.click(screen.getByRole('button', { name: T.send }));
await screen.findByText(T.sent);
const body = (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as FormData;
expect(body.get('clientKind')).toBe('desktop');
expect(body.get('clientOs')).toBe('windows');
expect(body.get('clientVersion')).toBe('1.2.0');
expect(body.get('clientCommit')).toBe('a6d1a64');
});
it('Test 13 (quick-260918-gza, alter Client): nur tessera_desktop ohne tessera_desktop_client -> desktop ohne Details', async () => {
document.cookie = 'tessera_desktop=1; path=/';
mockFetch.mockResolvedValue(jsonResponse(200, { sent: true }));
const user = userEvent.setup();
await renderButton();
await openDialog(user);
await user.click(screen.getByRole('button', { name: T.send }));
await screen.findByText(T.sent);
const body = (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as FormData;
expect(body.get('clientKind')).toBe('desktop');
expect(body.get('clientOs')).toBe('');
expect(body.get('clientVersion')).toBe('');
expect(body.get('clientCommit')).toBe('');
});
});
describe('computeCaptureSize (quick-260914-m97, reine Funktion)', () => {
@@ -5,6 +5,7 @@ import { useTranslations } from 'next-intl';
import { useEffect, useRef, useState } from 'react';
import { appVersion } from '@/lib/app-version';
import { dataUrlToBlob, sendBugReport } from '@/lib/bug-report-api';
import { getDesktopClientInfo, isDesktopClient } from '@/lib/desktop-client';
import { formatErrorsForReport } from '@/lib/error-buffer';
/**
@@ -54,6 +55,11 @@ export function BugReportDialog({ open, screenshot, isAdmin, onClose }: BugRepor
const handleSend = async () => {
setStatus('sending');
// Herkunft (quick-260918-gza): tessera_desktop entscheidet Desktop/
// Browser, tessera_desktop_client liefert die Details dazu; fehlt es
// (alter Client), bleibt es bei Desktop ohne Details.
const desktop = isDesktopClient();
const info = desktop ? getDesktopClientInfo() : null;
const result = await sendBugReport({
description: description.trim(),
page: window.location.pathname + window.location.search,
@@ -65,6 +71,10 @@ export function BugReportDialog({ open, screenshot, isAdmin, onClose }: BugRepor
clientTime: new Date().toISOString(),
errors: formatErrorsForReport(),
screenshot: attach && screenshot ? dataUrlToBlob(screenshot) : null,
clientKind: desktop ? 'desktop' : 'browser',
clientOs: info?.os ?? '',
clientVersion: info?.version ?? '',
clientCommit: info?.commit ?? '',
});
if (result.ok) {
setStatus('sent');
+87
View File
@@ -0,0 +1,87 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { sendBugReport } from './bug-report-api';
/**
* bug-report-api.test — NEU (quick-260918-gza). Bisher pruefte nur der
* Komponententest (`bug-report-button.test.tsx`) die FormData von
* `sendBugReport` — die reinen Nutzlastfelder (insbesondere die vier neuen
* Herkunftsfelder) gehoeren aber an die Funktion selbst, unabhaengig vom
* Dialog. Drei Tests: Desktop-Nutzlast, Browser-Nutzlast, Netzwerk-/
* HTTP-Fehler.
*/
const { mockFetch } = vi.hoisted(() => ({ mockFetch: vi.fn() }));
const basePayload = {
description: 'x',
page: '/a',
webVersion: 'v1',
webChannel: 'beta',
webCommit: 'c',
userAgent: 'UA',
viewport: '1x1',
clientTime: 't',
errors: ['e1', 'e2'],
screenshot: null,
};
beforeEach(() => {
mockFetch.mockReset();
mockFetch.mockResolvedValue(new Response('{}', { status: 200 }));
vi.stubGlobal('fetch', mockFetch);
});
afterEach(() => {
vi.unstubAllGlobals();
});
describe('sendBugReport (quick-260918-gza)', () => {
it('Test 1: Desktop-Nutzlast — clientKind/clientOs/clientVersion/clientCommit im FormData, errors mehrfach, kein screenshot-Feld', async () => {
const result = await sendBugReport({
...basePayload,
clientKind: 'desktop',
clientOs: 'windows',
clientVersion: '1.2.0',
clientCommit: 'a6d1a64',
});
expect(result).toEqual({ ok: true });
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
expect(String(url).endsWith('/bug-reports')).toBe(true);
expect(init.credentials).toBe('include');
const body = init.body as FormData;
expect(body.get('clientKind')).toBe('desktop');
expect(body.get('clientOs')).toBe('windows');
expect(body.get('clientVersion')).toBe('1.2.0');
expect(body.get('clientCommit')).toBe('a6d1a64');
expect(body.getAll('errors')).toEqual(['e1', 'e2']);
expect(body.has('screenshot')).toBe(false);
});
it('Test 2: Browser-Nutzlast — die drei uebrigen Client-Felder sind vorhanden, aber leer (nicht weggelassen)', async () => {
await sendBugReport({
...basePayload,
clientKind: 'browser',
clientOs: '',
clientVersion: '',
clientCommit: '',
});
const body = (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as FormData;
expect(body.get('clientKind')).toBe('browser');
expect(body.get('clientOs')).toBe('');
expect(body.get('clientVersion')).toBe('');
expect(body.get('clientCommit')).toBe('');
});
it('Test 3: Netzwerkfehler -> { ok: false, status: 0 }; HTTP-Fehler -> { ok: false, status }', async () => {
mockFetch.mockRejectedValueOnce(new Error('offline'));
await expect(
sendBugReport({ ...basePayload, clientKind: 'browser', clientOs: '', clientVersion: '', clientCommit: '' }),
).resolves.toEqual({ ok: false, status: 0 });
mockFetch.mockResolvedValueOnce(new Response('', { status: 429 }));
await expect(
sendBugReport({ ...basePayload, clientKind: 'browser', clientOs: '', clientVersion: '', clientCommit: '' }),
).resolves.toEqual({ ok: false, status: 429 });
});
});
+14
View File
@@ -13,6 +13,12 @@
* — das Bild als Datei, alle uebrigen Felder als Text — mit Cookie
* (`credentials: 'include'`) und OHNE eigenen Content-Type-Header: die
* Multipart-Grenze setzt der Browser selbst.
*
* Seit quick-260918-gza traegt die Nutzlast zusaetzlich vier Herkunftsfelder
* (`clientKind`, `clientOs`, `clientVersion`, `clientCommit`) — im Browser
* `clientKind=browser` mit leeren uebrigen Feldern, in der Desktop-App aus
* `getDesktopClientInfo()` (apps/web/src/lib/desktop-client.ts). Die API
* leitet daraus das Betreff-Kuerzel und die Zeile `Herkunft:` ab.
*/
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
@@ -79,6 +85,10 @@ export interface BugReportPayload {
clientTime: string;
errors: string[];
screenshot: Blob | null;
clientKind: 'desktop' | 'browser';
clientOs: string;
clientVersion: string;
clientCommit: string;
}
export type BugReportResult = { ok: true } | { ok: false; status: number };
@@ -93,6 +103,10 @@ export async function sendBugReport(p: BugReportPayload): Promise<BugReportResul
body.append('userAgent', p.userAgent);
body.append('viewport', p.viewport);
body.append('clientTime', p.clientTime);
body.append('clientKind', p.clientKind);
body.append('clientOs', p.clientOs);
body.append('clientVersion', p.clientVersion);
body.append('clientCommit', p.clientCommit);
for (const line of p.errors) body.append('errors', line);
if (p.screenshot) body.append('screenshot', p.screenshot, 'screenshot.png');
+58 -1
View File
@@ -1,14 +1,24 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { DESKTOP_COOKIE_NAME, isDesktopClient, useIsDesktopClient } from './desktop-client';
import {
DESKTOP_CLIENT_COOKIE_NAME,
DESKTOP_COOKIE_NAME,
getDesktopClientInfo,
isDesktopClient,
parseDesktopClientCookie,
useIsDesktopClient,
} from './desktop-client';
/**
* desktop-client.test — Gegenstueck zur Middleware (260917-h2s): liest das
* Cookie `tessera_desktop`, das die Middleware auf jede Antwort setzt, wenn
* der Desktop-Client `?desktop=1` an seine erste Navigation anhaengt.
* Seit quick-260918-gza zusaetzlich `parseDesktopClientCookie()` /
* `getDesktopClientInfo()` fuer das zweite Cookie `tessera_desktop_client`.
*/
function clearCookie() {
document.cookie = `${DESKTOP_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
document.cookie = `${DESKTOP_CLIENT_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
}
afterEach(() => {
@@ -50,3 +60,50 @@ describe('useIsDesktopClient', () => {
expect(result.current).toBe(false);
});
});
describe('getDesktopClientInfo / parseDesktopClientCookie (quick-260918-gza)', () => {
it('kodierte Form (wie der Browser sie in document.cookie haelt)', () => {
expect(
parseDesktopClientCookie('tessera_desktop=1; tessera_desktop_client=1.2.0%7Ca6d1a64%7Cwindows'),
).toEqual({ version: '1.2.0', commit: 'a6d1a64', os: 'windows' });
});
it('rohe Form (nicht kodiert)', () => {
expect(parseDesktopClientCookie('tessera_desktop_client=1.2.0|a6d1a64|windows')).toEqual({
version: '1.2.0',
commit: 'a6d1a64',
os: 'windows',
});
});
it('leerer Commit bleibt leer', () => {
expect(parseDesktopClientCookie('tessera_desktop_client=1.2.0%7C%7Clinux')).toEqual({
version: '1.2.0',
commit: '',
os: 'linux',
});
});
it('ohne Cookie: null', () => {
expect(parseDesktopClientCookie('tessera_desktop=1')).toBeNull();
});
it('unvollstaendige oder leere Pflichtfelder: null', () => {
expect(parseDesktopClientCookie('tessera_desktop_client=abc')).toBeNull();
expect(parseDesktopClientCookie('tessera_desktop_client=1.2.0|x')).toBeNull();
expect(parseDesktopClientCookie('tessera_desktop_client=|a6d1a64|linux')).toBeNull();
expect(parseDesktopClientCookie('tessera_desktop_client=1.2.0|a6d1a64|')).toBeNull();
});
it('getDesktopClientInfo() liest document.cookie', () => {
expect(getDesktopClientInfo()).toBeNull();
document.cookie = 'tessera_desktop_client=1.2.0%7Ca6d1a64%7Cwindows; path=/';
expect(getDesktopClientInfo()).toEqual({ version: '1.2.0', commit: 'a6d1a64', os: 'windows' });
});
it('getDesktopClientInfo() ohne document: null', () => {
vi.stubGlobal('document', undefined);
expect(getDesktopClientInfo()).toBeNull();
vi.unstubAllGlobals();
});
});
+52
View File
@@ -9,6 +9,58 @@ import { useEffect, useState } from 'react';
*/
export const DESKTOP_COOKIE_NAME = 'tessera_desktop';
/**
* Gegenstueck zu `buildDesktopClientCookieValue()` in der Middleware
* (quick-260918-gza): dort werden `dv`/`dc`/`dos` mit `${dv}|${dc}|${dos}`
* zusammengesetzt und von Next per `encodeURIComponent` serialisiert --
* `|` wird `%7C` (gemessen). `document.cookie` liefert deshalb die
* KODIERTE Form; `parseDesktopClientCookie()` dekodiert (in try/catch,
* falls der Wert bereits roh vorliegt, z. B. direkt gesetzt im Test).
*/
export const DESKTOP_CLIENT_COOKIE_NAME = 'tessera_desktop_client';
export interface DesktopClientInfo {
version: string;
commit: string;
os: string;
}
/**
* Parst den Wert des Cookies `tessera_desktop_client` aus einem rohen
* `document.cookie`-String. `null` bei fehlendem Cookie oder einer Form,
* die nicht genau drei durch `|` getrennte Teile mit nicht-leerer Version
* und nicht-leerem Betriebssystem ergibt (Commit darf leer sein).
*/
export function parseDesktopClientCookie(cookieString: string): DesktopClientInfo | null {
const entry = cookieString
.split(';')
.map((c) => c.trim())
.find((c) => c.startsWith(`${DESKTOP_CLIENT_COOKIE_NAME}=`));
if (!entry) return null;
const raw = entry.slice(DESKTOP_CLIENT_COOKIE_NAME.length + 1);
let value = raw;
try {
value = decodeURIComponent(raw);
} catch {
// Rohwert (z. B. bereits dekodiert im Test) -- unveraendert weiterverwenden.
}
const parts = value.split('|');
if (parts.length !== 3) return null;
const [version, commit, os] = parts;
if (!version || !os) return null;
return { version, commit, os };
}
/** `null` ohne `document` (SSR) oder ohne gueltiges Cookie. */
export function getDesktopClientInfo(): DesktopClientInfo | null {
if (typeof document === 'undefined') {
return null;
}
return parseDesktopClientCookie(document.cookie);
}
export function isDesktopClient(): boolean {
if (typeof document === 'undefined') {
return false;
+48
View File
@@ -65,4 +65,52 @@ describe('middleware — Desktop-Client-Cookie (260917-h2s)', () => {
expect(res.headers.get('set-cookie')).toContain('tessera_desktop=1');
expect(res.headers.get('x-middleware-next')).toBe('1');
});
it('Test 6 (quick-260918-gza): /login mit dv/dc/dos setzt zusaetzlich tessera_desktop_client', async () => {
const req = new NextRequest('http://localhost:3000/login?desktop=1&dv=1.2.0&dc=a6d1a64&dos=windows');
const res = await middleware(req);
expect(res.cookies.get('tessera_desktop')?.value).toBe('1');
expect(res.cookies.get('tessera_desktop_client')?.value).toBe('1.2.0|a6d1a64|windows');
const setCookie = res.headers.get('set-cookie');
expect(setCookie).toContain('tessera_desktop_client=1.2.0%7Ca6d1a64%7Cwindows');
expect(setCookie).toContain('Max-Age=31536000');
expect(setCookie).toContain('Path=/');
// Kein HttpOnly fuer DIESES Cookie -- der bestehende tessera_desktop
// liefert ebenfalls kein HttpOnly, darum genuegt die Wertpruefung oben.
});
it('Test 7 (quick-260918-gza, alter Client): /login?desktop=1 ohne dv/dc/dos setzt kein tessera_desktop_client', async () => {
const req = new NextRequest('http://localhost:3000/login?desktop=1');
const res = await middleware(req);
expect(res.cookies.get('tessera_desktop')?.value).toBe('1');
expect(res.cookies.get('tessera_desktop_client')).toBeUndefined();
});
it('Test 8 (quick-260918-gza, Bereinigung): ungueltige oder fehlende Werte setzen kein tessera_desktop_client', async () => {
const angleBrackets = new NextRequest(
'http://localhost:3000/login?desktop=1&dv=1.2.0%3Cscript%3E&dc=a6d1a64&dos=windows',
);
expect((await middleware(angleBrackets)).cookies.get('tessera_desktop_client')).toBeUndefined();
const spaceInOs = new NextRequest(
'http://localhost:3000/login?desktop=1&dv=1.2.0&dc=a6d1a64&dos=win%20dows',
);
expect((await middleware(spaceInOs)).cookies.get('tessera_desktop_client')).toBeUndefined();
const missingDv = new NextRequest('http://localhost:3000/login?desktop=1&dc=a6d1a64&dos=windows');
expect((await middleware(missingDv)).cookies.get('tessera_desktop_client')).toBeUndefined();
const emptyCommit = new NextRequest('http://localhost:3000/login?desktop=1&dv=1.2.0&dc=&dos=linux');
expect((await middleware(emptyCommit)).cookies.get('tessera_desktop_client')?.value).toBe('1.2.0||linux');
});
it('Test 9 (quick-260918-gza, Redirect-Pfad): /dashboard ohne Session setzt beide Cookies auf dem 307', async () => {
const req = new NextRequest('http://localhost:3000/dashboard?desktop=1&dv=1.2.0&dc=a6d1a64&dos=linux');
const res = await middleware(req);
expect(res.status).toBe(307);
expect(res.headers.get('location')).toContain('/login');
expect(res.cookies.get('tessera_desktop')?.value).toBe('1');
expect(res.cookies.get('tessera_desktop_client')?.value).toBe('1.2.0|a6d1a64|linux');
});
});
+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;
}