- 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
97 lines
3.2 KiB
TypeScript
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).*)",
|
|
],
|
|
};
|