Das Logo war nie kaputt — der Proxy hat es umgeleitet
`curl -sI` auf /brand/manner-logo.png antwortete mit **307**. Die Datei lag die ganze Zeit da, ausgeliefert wurde sie nie: der Abgleich in proxy.ts nimmt alles ausser _next/static, _next/image und favicon.ico, und public/ stand nicht darin. Eine Anfrage nach dem Logo lief also in den Gate, fand keine Sitzung und bekam eine Umleitung nach /login. Auf ein <img> antwortet der Server damit mit HTML, und der Browser zeigt den Ersatztext. Das erklaert auch, warum es so sprunghaft aussah. Auffallen konnte es nur an einer einzigen Stelle: die Anmeldeseite ist die einzige Seite, die jemand ohne Sitzung zu sehen bekommt — und ausgerechnet dort steht das Logo. Ueberall sonst ist man angemeldet, der Gate laesst das Bild durch. Einmal so geladen liegt es im Zwischenspeicher und erscheint auf /login weiter, bis jemand hart neu laedt. Und es erklaert, warum keine der Aenderungen am Bild geholfen hat: weder das Ausmisten des SVG noch der Wechsel auf ein PNG konnte etwas ausrichten, weil die Datei den Browser gar nicht erreichte. Dass es „seit 8016d31" auftrat, war eine Verwechslung von Ursache und Gelegenheit — dort bekam die Anmeldeseite ihre rosa Flaeche und damit ueberhaupt erst ein sichtbares Logo an einer Stelle ohne Sitzung. Aufgenommen wird jetzt, was eine nicht angemeldete Person auf der Anmeldeseite braucht: brand/ sowie icon.svg, apple-icon.png und manifest.webmanifest, die der Browser von sich aus holt. Einzeln aufgezaehlt und nicht als Regel ueber Dateiendungen — „alles mit einem Punkt darin" haette auch Routen durchgelassen, die keine Datei sind. In public/ liegt nichts Personenbezogenes und darf auch nie etwas liegen; alles, was Daten fuehrt, geht durch withUser() und die Policies. Fuenf Tests auf den Abgleich selbst. Die vorhandenen rufen proxy() unmittelbar auf und gehen damit am matcher vorbei — ein Fehler dort war von ihnen nicht zu sehen. Geprueft wird beides: dass die Dateien vorbeikommen, und dass sonst nichts aufgeht, auch nicht ueber einen aehnlich aussehenden Pfad wie /brandneu. Lint, Typen, Schemaabgleich, 567 Tests und der Build sind sauber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
37
proxy.ts
37
proxy.ts
@@ -78,6 +78,41 @@ export async function proxy(...args: Parameters<Awaited<typeof gate>>) {
|
|||||||
return (await gate)(...args);
|
return (await gate)(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══ Was der Proxy gar nicht erst sieht ═══
|
||||||
|
//
|
||||||
|
// Der Abgleich ist eine Verneinung: alles ausser dem hier Aufgezaehlten laeuft
|
||||||
|
// durch den Gate. Das ist die richtige Richtung — eine vergessene Route ist
|
||||||
|
// dann geschuetzt und nicht offen.
|
||||||
|
//
|
||||||
|
// Es fehlten allerdings die statischen Dateien aus public/. Eine Anfrage nach
|
||||||
|
// /brand/manner-logo.png lief damit in den Gate, fand keine Sitzung und
|
||||||
|
// bekam eine Umleitung nach /login. Der Browser bekommt auf ein <img> also
|
||||||
|
// HTML statt eines Bildes und zeigt den Ersatztext.
|
||||||
|
//
|
||||||
|
// Aufgefallen ist es nur an einer Stelle, und zwar an der einzigen, an der es
|
||||||
|
// auffallen konnte: die Anmeldeseite ist die einzige Seite, die jemand ohne
|
||||||
|
// Sitzung zu sehen bekommt — und ausgerechnet dort steht das Logo. Ueberall
|
||||||
|
// sonst ist man angemeldet, der Gate laesst das Bild durch, und es erscheint.
|
||||||
|
// Daher auch das Sprunghafte: einmal angemeldet geladen, liegt es im
|
||||||
|
// Zwischenspeicher des Browsers und erscheint auf /login weiter, bis jemand
|
||||||
|
// hart neu laedt.
|
||||||
|
//
|
||||||
|
// Aufgenommen wird nur, was eine nicht angemeldete Person auf der
|
||||||
|
// Anmeldeseite braucht, und jeder Eintrag einzeln statt einer Regel ueber
|
||||||
|
// Dateiendungen: „alles mit einem Punkt darin" haette auch Routen
|
||||||
|
// durchgelassen, die keine Datei sind.
|
||||||
|
//
|
||||||
|
// _next/static, _next/image die Ausgabe des Bauwerkzeugs
|
||||||
|
// favicon.ico, icon.svg, die Symbole, die der Browser von sich aus holt
|
||||||
|
// apple-icon.png
|
||||||
|
// manifest.webmanifest dito, fuer die Installation als App
|
||||||
|
// brand/ Logo und App-Symbole aus public/brand
|
||||||
|
//
|
||||||
|
// In public/ liegt sonst nichts; personenbezogen ist dort nichts und darf
|
||||||
|
// dort auch nie etwas liegen — alles, was Daten fuehrt, geht durch withUser()
|
||||||
|
// und die Policies.
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
matcher: [
|
||||||
|
"/((?!_next/static|_next/image|favicon\\.ico|icon\\.svg|apple-icon\\.png|manifest\\.webmanifest|brand/).*)",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,10 +27,12 @@ type Session = { user: { id: string } } | null;
|
|||||||
// Signatur eines Route Handlers (Anfrage plus Kontext), und der zweite
|
// Signatur eines Route Handlers (Anfrage plus Kontext), und der zweite
|
||||||
// Parameter spielt hier keine Rolle.
|
// Parameter spielt hier keine Rolle.
|
||||||
let proxy: (req: NextRequest) => unknown;
|
let proxy: (req: NextRequest) => unknown;
|
||||||
|
let matcher: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const mod = await import("@/proxy");
|
const mod = await import("@/proxy");
|
||||||
proxy = mod.proxy as unknown as typeof proxy;
|
proxy = mod.proxy as unknown as typeof proxy;
|
||||||
|
matcher = (mod.config as { matcher: string[] }).matcher[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
async function visit(path: string, session: Session): Promise<Response> {
|
async function visit(path: string, session: Session): Promise<Response> {
|
||||||
@@ -93,3 +95,51 @@ describe("proxy", () => {
|
|||||||
expect(new URL(res.headers.get("location")!).pathname).toBe("/login");
|
expect(new URL(res.headers.get("location")!).pathname).toBe("/login");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ═══ Der Abgleich selbst ═══
|
||||||
|
//
|
||||||
|
// Die Tests darueber rufen proxy() unmittelbar auf und gehen damit am
|
||||||
|
// `matcher` vorbei — der entscheidet aber, ob der Gate ueberhaupt laeuft. Ein
|
||||||
|
// Fehler dort ist deshalb von den Tests oben nicht zu sehen, und genau einer
|
||||||
|
// ist passiert: die Dateien aus public/ standen nicht in der Ausnahmeliste.
|
||||||
|
// Eine Anfrage nach /brand/manner-logo.png lief in den Gate, fand keine
|
||||||
|
// Sitzung und wurde nach /login umgeleitet; auf der Anmeldeseite — der
|
||||||
|
// einzigen Seite ohne Sitzung — blieb das Logo deshalb leer.
|
||||||
|
describe("config.matcher", () => {
|
||||||
|
// Erst im Test gebaut, nicht beim Einsammeln: `matcher` steht erst nach dem
|
||||||
|
// beforeAll oben zur Verfuegung.
|
||||||
|
const laeuftDurchDenGate = (pfad: string) => new RegExp(`^${matcher}$`).test(pfad);
|
||||||
|
|
||||||
|
it("laesst die statischen Dateien aus public/ am Gate vorbei", () => {
|
||||||
|
// Ohne diese Ausnahme ist das Logo auf der Anmeldeseite eine Umleitung.
|
||||||
|
expect(laeuftDurchDenGate("/brand/manner-logo.png")).toBe(false);
|
||||||
|
expect(laeuftDurchDenGate("/brand/icon-512.png")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("laesst die Symbole vorbei, die der Browser von sich aus holt", () => {
|
||||||
|
for (const pfad of ["/favicon.ico", "/icon.svg", "/apple-icon.png", "/manifest.webmanifest"]) {
|
||||||
|
expect(laeuftDurchDenGate(pfad), pfad).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("laesst die Ausgabe des Bauwerkzeugs vorbei", () => {
|
||||||
|
expect(laeuftDurchDenGate("/_next/static/chunks/main.js")).toBe(false);
|
||||||
|
expect(laeuftDurchDenGate("/_next/image?url=x")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Die andere Haelfte, und die wichtigere: die Ausnahmen duerfen nichts
|
||||||
|
// aufmachen, was Daten fuehrt.
|
||||||
|
it("schickt alles Uebrige weiterhin durch den Gate", () => {
|
||||||
|
for (const pfad of ["/", "/employees", "/employees/123", "/audit", "/reports", "/import",
|
||||||
|
"/api/export/employees", "/api/import", "/passwort-aendern", "/login"]) {
|
||||||
|
expect(laeuftDurchDenGate(pfad), pfad).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("laesst sich nicht mit einem aehnlich aussehenden Pfad umgehen", () => {
|
||||||
|
// Kein Prefix-Treffer auf halbem Weg: /brandneu ist keine Datei aus
|
||||||
|
// public/brand, und /icon.svg.php ist kein Symbol.
|
||||||
|
expect(laeuftDurchDenGate("/brandneu")).toBe(true);
|
||||||
|
expect(laeuftDurchDenGate("/employees/brand/1")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user