Files
famapp/src/middleware.ts
T
ginnoir ec2f3930ad feat: proper logo, favicon, and pwa icons
- replace placeholder house svg with clean geometric house mark
  on indigo-700 (#4338CA) — roof triangle, body, door cutout
- add favicon.svg with prefers-color-scheme dark variant
- add icon-16/32 sizes; regenerate all pngs from svg via sharp
- update BrandMark component to use inline svg house instead of 'f'
- wire favicon + sized pngs into layout metadata
- fix middleware to allow icon/manifest/favicon paths without auth
  (android launcher fetches icons in a cookieless system context)
- fix getCurrentSession() to redirect to /login instead of throwing
  when session cookie is stale/invalid
2026-06-03 15:59:34 -05:00

97 lines
3.2 KiB
TypeScript

import { NextResponse, type NextRequest } from "next/server";
import { consume } from "@/lib/rate-limit";
const PUBLIC_PREFIXES = ["/api/auth/", "/s/", "/icon-", "/favicon"];
const PUBLIC_PATHS = new Set(["/login", "/manifest.webmanifest", "/offline.html"]);
const SESSION_COOKIE_NAMES = ["authjs.session-token", "__Secure-authjs.session-token"];
// Token-prefix length used as part of the rate-limit bucket key.
const RL_PREFIX_LEN = 8;
export function middleware(request: NextRequest) {
const start = performance.now();
const response = route(request);
logRequest(request, response.status, Math.round(performance.now() - start));
return response;
}
function withPathname(response: NextResponse, pathname: string): NextResponse {
response.headers.set("x-pathname", pathname);
return response;
}
function route(request: NextRequest): NextResponse {
const { pathname } = request.nextUrl;
// Rate-limit the public share-link viewer (/s/<token>).
// The rate limiter runs in the Edge runtime and has its own module-level bucket
// Map (separate from the Node.js runtime used by page components). All requests
// to /s/<token> are counted here regardless of whether the token resolves; page
// components additionally track per-failure counts via recordFailure() for
// accurate auditing. Switch to Redis for cross-runtime / multi-replica accuracy.
if (pathname.startsWith("/s/") && pathname.length > 3) {
const token = pathname.slice(3); // strip "/s/"
const ip = clientIp(request);
const key = `${ip}:${token.slice(0, RL_PREFIX_LEN)}`;
if (!consume(key)) {
return new NextResponse("Too Many Requests", {
status: 429,
headers: { "Retry-After": "60", "Content-Type": "text/plain" },
});
}
}
if (PUBLIC_PATHS.has(pathname) || PUBLIC_PREFIXES.some((p) => pathname.startsWith(p))) {
return withPathname(NextResponse.next(), pathname);
}
if (hasSessionCookie(request)) return withPathname(NextResponse.next(), pathname);
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", request.url);
return NextResponse.redirect(loginUrl);
}
function clientIp(request: NextRequest): string {
return (
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
request.headers.get("x-real-ip") ??
"0.0.0.0"
);
}
function hasSessionCookie(request: NextRequest) {
return request.cookies
.getAll()
.some((cookie) =>
SESSION_COOKIE_NAMES.some(
(name) => cookie.name === name || cookie.name.startsWith(`${name}.`),
),
);
}
// Structured request log — output as JSON so it's machine-readable in production.
// pino is not available in the Edge runtime; console.log goes to server stdout.
// userId is omitted: decoding the session token requires a DB lookup unavailable here.
function logRequest(request: NextRequest, status: number, ms: number) {
const authenticated = hasSessionCookie(request);
const entry = {
level: "info",
time: Date.now(),
method: request.method,
path: request.nextUrl.pathname,
status,
ms,
authenticated,
};
console.log(JSON.stringify(entry));
}
export const config = {
matcher: [
// Skip Next.js internals and static files
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};