feat(web): Desktop-Client per Cookie erkennen — Download-Links und Browser-Kontextmenü in der App aus
- withDesktopCookie in middleware.ts setzt tessera_desktop=1 auf JEDER Antwort (Fruehausstieg, Redirects, next()), wenn ?desktop=1 anliegt - desktop-client.ts: isDesktopClient() liest das Cookie, useIsDesktopClient() kapselt es hydration-sicher per useEffect - DesktopDownloadLinks fragt /desktop/latest im Desktop-Client gar nicht erst an und rendert nichts - DesktopContextMenuGuard unterdrueckt das WebView2-Kontextmenue ausserhalb von Eingabefeldern/contenteditable, in layout.tsx eingebunden - middleware.test.ts (neu), desktop-client.test.ts (neu), desktop-context-menu-guard.test.tsx (neu), Test 4 in desktop-download-links.test.tsx — alle 417 Web-Tests und type-check gruen Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,31 @@ import { buildNextParam } from '@/lib/safe-next';
|
||||
|
||||
const publicRoutes = ['/login', '/reset-password'];
|
||||
|
||||
const DESKTOP_COOKIE = 'tessera_desktop';
|
||||
|
||||
/**
|
||||
* Setzt das Cookie `tessera_desktop`, wenn die Anfrage `?desktop=1` traegt
|
||||
* (260917-h2s). Der Desktop-Client (apps/desktop/src-tauri/src/lib.rs,
|
||||
* `with_desktop_marker`) haengt den Parameter nur an seine ERSTE Navigation
|
||||
* an; das Cookie muss deshalb auf JEDER Antwort landen, auch auf dem
|
||||
* 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.
|
||||
*/
|
||||
function withDesktopCookie(req: NextRequest, res: NextResponse): NextResponse {
|
||||
if (req.nextUrl.searchParams.get('desktop') === '1') {
|
||||
res.cookies.set(DESKTOP_COOKIE, '1', {
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
sameSite: 'lax',
|
||||
httpOnly: false,
|
||||
secure: req.nextUrl.protocol === 'https:',
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Umleitung zur Anmeldeseite mit `next`-Parameter (quick-260917-gyd): Pfad
|
||||
* + Query der urspruenglich angeforderten Seite wandern mit, damit die
|
||||
@@ -42,7 +67,7 @@ export async function middleware(req: NextRequest) {
|
||||
|
||||
// Allow public routes without authentication
|
||||
if (publicRoutes.some((route) => path.startsWith(route))) {
|
||||
return NextResponse.next();
|
||||
return withDesktopCookie(req, NextResponse.next());
|
||||
}
|
||||
|
||||
// Skip static assets and API routes (handled by NestJS)
|
||||
@@ -52,14 +77,14 @@ export async function middleware(req: NextRequest) {
|
||||
path.startsWith('/favicon.ico') ||
|
||||
path.startsWith('/api')
|
||||
) {
|
||||
return NextResponse.next();
|
||||
return withDesktopCookie(req, NextResponse.next());
|
||||
}
|
||||
|
||||
// Read session cookie
|
||||
const session = req.cookies.get('session')?.value;
|
||||
|
||||
if (!session) {
|
||||
return redirectToLogin(req);
|
||||
return withDesktopCookie(req, redirectToLogin(req));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -72,15 +97,18 @@ export async function middleware(req: NextRequest) {
|
||||
payload.mustChangePassword === true &&
|
||||
!path.startsWith('/change-password')
|
||||
) {
|
||||
return NextResponse.redirect(new URL('/change-password', req.nextUrl));
|
||||
return withDesktopCookie(
|
||||
req,
|
||||
NextResponse.redirect(new URL('/change-password', req.nextUrl)),
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
return withDesktopCookie(req, NextResponse.next());
|
||||
} catch {
|
||||
// JWT verification failed -- clear stale cookie and redirect to login
|
||||
const response = redirectToLogin(req);
|
||||
response.cookies.delete('session');
|
||||
return response;
|
||||
return withDesktopCookie(req, response);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user