feat(auth): restrict public routes

This commit is contained in:
Haitao Pan 2026-04-12 19:30:07 +08:00
parent ddb2a7b627
commit a0e6da97b1
2 changed files with 112 additions and 0 deletions

55
middleware.ts Normal file
View File

@ -0,0 +1,55 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { SESSION_COOKIE_NAME } from "./src/lib/authGateway";
const PUBLIC_EXACT_PATHS = new Set([
"/",
"/services",
"/login",
"/register",
"/email-verification",
"/logout",
"/404",
"/500",
]);
function isDocsPath(pathname: string): boolean {
return pathname === "/docs" || pathname.startsWith("/docs/");
}
function isPublicPath(pathname: string): boolean {
return PUBLIC_EXACT_PATHS.has(pathname) || isDocsPath(pathname);
}
function buildRedirectTarget(request: NextRequest): string {
const query = request.nextUrl.search;
return `${request.nextUrl.pathname}${query}`;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (isPublicPath(pathname)) {
return undefined;
}
const token = request.cookies.get(SESSION_COOKIE_NAME)?.value?.trim();
if (token) {
return undefined;
}
const loginUrl = new URL("/login", request.url);
const redirect = buildRedirectTarget(request);
if (redirect && redirect !== "/login") {
loginUrl.searchParams.set("redirect", redirect);
}
return NextResponse.redirect(loginUrl);
}
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|.*\\.(?:css|gif|ico|jpg|jpeg|js|map|png|svg|txt|webp|woff|woff2|xml)$).*)",
],
};

57
src/middleware.test.ts Normal file
View File

@ -0,0 +1,57 @@
// @vitest-environment node
import { describe, expect, it } from "vitest";
import { NextRequest } from "next/server";
import { middleware } from "../middleware";
describe("middleware public route policy", () => {
it("keeps homepage public", () => {
const response = middleware(new NextRequest("https://console.svc.plus/"));
expect(response).toBeUndefined();
});
it("keeps docs routes public", () => {
const response = middleware(
new NextRequest("https://console.svc.plus/docs/getting-started"),
);
expect(response).toBeUndefined();
});
it("keeps only the top-level services page public", () => {
const publicResponse = middleware(
new NextRequest("https://console.svc.plus/services"),
);
const protectedResponse = middleware(
new NextRequest("https://console.svc.plus/services/openclaw"),
);
expect(publicResponse).toBeUndefined();
expect(protectedResponse?.status).toBe(307);
expect(protectedResponse?.headers.get("location")).toContain(
"/login?redirect=%2Fservices%2Fopenclaw",
);
});
it("redirects protected pages to login when no session cookie exists", () => {
const response = middleware(
new NextRequest("https://console.svc.plus/panel?tab=agent"),
);
expect(response?.status).toBe(307);
expect(response?.headers.get("location")).toContain(
"/login?redirect=%2Fpanel%3Ftab%3Dagent",
);
});
it("allows protected pages when a session cookie exists", () => {
const request = new NextRequest("https://console.svc.plus/support");
request.cookies.set("xc_session", "token");
const response = middleware(request);
expect(response).toBeUndefined();
});
});