fix(web): /login leitet angemeldete Benutzer aufs Dashboard (bzw. sicheres next)
Tessera CI/CD / Lint & Type Check (push) Successful in 53s
Tessera CI/CD / Tests (push) Successful in 1m30s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 20s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m28s

Nur bei gueltiger Signatur und ohne ausstehenden Kennwortwechsel; next ueber
sanitizeNextPath, /login als Ziel -> Dashboard. Gesperrte Konten: API lehnt
ab, Oberflaeche loescht das Cookie serverseitig, keine Schleife.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 16:30:44 +02:00
parent 52f538c432
commit 31d514b7ca
3 changed files with 106 additions and 12 deletions
+79 -6
View File
@@ -1,6 +1,7 @@
// @vitest-environment node
import { NextRequest } from 'next/server';
import { SignJWT } from 'jose';
import { NextRequest } from 'next/server';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { middleware } from './middleware';
@@ -67,7 +68,9 @@ describe('middleware — Desktop-Client-Cookie (260917-h2s)', () => {
});
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 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');
@@ -98,15 +101,23 @@ describe('middleware — Desktop-Client-Cookie (260917-h2s)', () => {
);
expect((await middleware(spaceInOs)).cookies.get('tessera_desktop_client')).toBeUndefined();
const missingDv = new NextRequest('http://localhost:3000/login?desktop=1&dc=a6d1a64&dos=windows');
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');
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 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');
@@ -114,3 +125,65 @@ describe('middleware — Desktop-Client-Cookie (260917-h2s)', () => {
expect(res.cookies.get('tessera_desktop_client')?.value).toBe('1.2.0|a6d1a64|linux');
});
});
describe('middleware — /login bei bestehender Anmeldung (quick-260930)', () => {
beforeEach(() => {
vi.stubEnv('JWT_SECRET', 'test-secret');
});
afterEach(() => {
vi.unstubAllEnvs();
});
async function token(claims: Record<string, unknown> = {}) {
return new SignJWT({ sub: 'u1', ...claims })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('5m')
.sign(new TextEncoder().encode('test-secret'));
}
function loginReq(url: string, session?: string) {
return new NextRequest(url, session ? { headers: { cookie: `session=${session}` } } : {});
}
it('angemeldet: /login leitet aufs Dashboard um', async () => {
const res = await middleware(loginReq('http://localhost:3000/login', await token()));
expect(res.status).toBe(307);
expect(new URL(res.headers.get('location') as string).pathname).toBe('/');
});
it('angemeldet mit sicherem next: dorthin', async () => {
const res = await middleware(
loginReq('http://localhost:3000/login?next=%2Fadmin%2Fusers', await token()),
);
expect(new URL(res.headers.get('location') as string).pathname).toBe('/admin/users');
});
it('angemeldet mit fremdem oder zirkulaerem next: Dashboard', async () => {
for (const next of ['https%3A%2F%2Fboese.example', '%2F%2Fboese.example', '%2Flogin']) {
const res = await middleware(
loginReq(`http://localhost:3000/login?next=${next}`, await token()),
);
const loc = new URL(res.headers.get('location') as string);
expect(loc.host).toBe('localhost:3000');
expect(loc.pathname).toBe('/');
}
});
it('ohne oder mit ungueltigem Cookie: Anmeldeseite wie bisher', async () => {
const ohne = await middleware(loginReq('http://localhost:3000/login'));
expect(ohne.headers.get('x-middleware-next')).toBe('1');
const kaputt = await middleware(
loginReq('http://localhost:3000/login', 'kein.gueltiges.token'),
);
expect(kaputt.headers.get('x-middleware-next')).toBe('1');
});
it('Kennwortwechsel ausstehend: keine Umleitung von /login', async () => {
const res = await middleware(
loginReq('http://localhost:3000/login', await token({ mustChangePassword: true })),
);
expect(res.headers.get('x-middleware-next')).toBe('1');
});
});