Grundgerüst mit Server, PIN-Anmeldung und Docker-Deploy aufbauen

Die alte Clip-Auswahl-Oberfläche entfällt. Neu sind ein Express-Server,
der im Entwicklungsbetrieb Vite einbindet, eine PIN-Anmeldung mit
signiertem Sitzungs-Cookie und ein Docker-Image mit ffmpeg für VCH.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ferdi2goandClaude Opus 5.5 committed 2026-09-29 09:15:01 +00:00
1 parent 8ed1396b1c
commit f7d249f28b
23 files changed
+4456 -1358

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
import assert from 'node:assert/strict';
import { after, before, test } from 'node:test';
import { createApp } from '../server/app.js';
let server;
let baseUrl;
before(async () => {
const app = createApp({ config: { pin: '4711' }, secret: 'testgeheimnis' });
server = app.listen(0);
await new Promise((resolve) => server.once('listening', resolve));
baseUrl = `http://127.0.0.1:${server.address().port}`;
});
after(() => server.close());
const login = (pin) =>
fetch(`${baseUrl}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pin }),
});
test('Healthcheck antwortet ohne Anmeldung', async () => {
const res = await fetch(`${baseUrl}/healthz`);
assert.equal(res.status, 200);
});
test('Geschützte Routen verlangen eine Anmeldung', async () => {
const res = await fetch(`${baseUrl}/api/irgendwas`);
assert.equal(res.status, 401);
});
test('Falsche PIN wird abgelehnt', async () => {
const res = await login('0000');
assert.equal(res.status, 401);
assert.equal(res.headers.get('set-cookie'), null);
});
test('Richtige PIN liefert ein Sitzungs-Cookie, das gilt', async () => {
const res = await login('4711');
assert.equal(res.status, 200);
const cookie = res.headers.get('set-cookie').split(';')[0];
assert.match(cookie, /^fdc_session=/);
assert.match(res.headers.get('set-cookie'), /HttpOnly/);
const session = await fetch(`${baseUrl}/api/session`, { headers: { cookie } });
assert.deepEqual(await session.json(), { authenticated: true, pinConfigured: true });
const protectedRes = await fetch(`${baseUrl}/api/irgendwas`, { headers: { cookie } });
assert.equal(protectedRes.status, 404);
});
+44
View File
@@ -0,0 +1,44 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { createLoginLimiter, createToken, pinMatches, verifyToken } from '../server/auth.js';
test('Token ist mit Geheimnis und PIN gültig', () => {
const token = createToken('geheim', '1234');
assert.equal(verifyToken('geheim', '1234', token), true);
});
test('Token wird nach PIN-Wechsel ungültig', () => {
const token = createToken('geheim', '1234');
assert.equal(verifyToken('geheim', '9999', token), false);
});
test('Token mit fremdem Geheimnis oder manipuliertem Ablauf ist ungültig', () => {
const token = createToken('geheim', '1234');
assert.equal(verifyToken('anderes', '1234', token), false);
const [, signature] = token.split('.');
assert.equal(verifyToken('geheim', '1234', `${Date.now() + 1e12}.${signature}`), false);
});
test('Abgelaufenes Token ist ungültig', () => {
const token = createToken('geheim', '1234', 0);
assert.equal(verifyToken('geheim', '1234', token), false);
});
test('Ohne gesetzte PIN gilt kein Token und keine Eingabe', () => {
assert.equal(verifyToken('geheim', '', createToken('geheim', '')), false);
assert.equal(pinMatches('', ''), false);
});
test('PIN-Vergleich', () => {
assert.equal(pinMatches('1234', '1234'), true);
assert.equal(pinMatches('1234', '1235'), false);
assert.equal(pinMatches('1234', undefined), false);
});
test('Sperre nach zu vielen Fehlversuchen läuft ab', () => {
const limiter = createLoginLimiter({ maxFailures: 2, windowMs: 1000 });
limiter.recordFailure(0);
limiter.recordFailure(10);
assert.equal(limiter.isBlocked(500), true);
assert.equal(limiter.isBlocked(1500), false);
});