chore: Remove /api/ routes from proxy (#27883)
* chore: Remove /api/ routes from proxy * Update apps/web/proxy.ts Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * Add tests: apps/web/proxy.test.ts Generated by Paragon from proposal for PR #27883 * Update proxy.test.ts * fix: update proxy tests to account for /api/auth/signup in matcher Co-Authored-By: unknown <> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
cubic-dev-ai[bot]
unknown
Devin AI
parent
be0b707296
commit
8a3944f987
+18
-61
@@ -6,7 +6,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import type { Mock } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
// We'll test the wrapped proxy as it would be used in production
|
||||
import proxy, { checkPostMethod, POST_METHODS_ALLOWED_API_ROUTES } from "./proxy";
|
||||
import proxy from "./proxy";
|
||||
import { config } from "./proxy";
|
||||
|
||||
// Mock dependencies at module level
|
||||
@@ -146,34 +146,6 @@ const callProxy = async (req: NextRequest): Promise<Response> => {
|
||||
return (await (proxy as any)(req)) as Response;
|
||||
};
|
||||
|
||||
describe("Middleware - POST requests restriction", () => {
|
||||
const createRequest = (path: string, method: string) => {
|
||||
return new NextRequest(
|
||||
new Request(`${WEBAPP_URL}${path}`, {
|
||||
method,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
it("should allow POST requests to /api routes", async () => {
|
||||
const req1 = createRequest("/api/auth/signup", "POST");
|
||||
const res1 = checkPostMethod(req1);
|
||||
expect(res1).toBeNull();
|
||||
});
|
||||
|
||||
it("should allow GET requests to app routes", async () => {
|
||||
const req = createRequest("/team/xyz", "GET");
|
||||
const res = checkPostMethod(req);
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
|
||||
it("should allow GET requests to /api routes", async () => {
|
||||
const req = createRequest("/api/auth/signup", "GET");
|
||||
const res = checkPostMethod(req);
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Middleware Integration Tests", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -486,9 +458,11 @@ describe("Middleware Matcher Configuration", () => {
|
||||
|
||||
it("should include all core middleware routes", () => {
|
||||
expect(matcher).toContain("/auth/login");
|
||||
expect(matcher).toContain("/login");
|
||||
expect(matcher).toContain("/apps/installed");
|
||||
expect(matcher).toContain("/auth/logout");
|
||||
expect(matcher).toContain("/api/auth/signup");
|
||||
expect(matcher).toContain("/apps/installed");
|
||||
expect(matcher).toContain("/availability");
|
||||
expect(matcher).toContain("/login");
|
||||
expect(matcher).toContain("/:path*/embed");
|
||||
});
|
||||
|
||||
@@ -497,37 +471,20 @@ describe("Middleware Matcher Configuration", () => {
|
||||
expect(uniqueEntries.size).toBe(matcher.length);
|
||||
});
|
||||
|
||||
it("should cover every POST_METHODS_ALLOWED_API_ROUTES route via exact match or wildcard", () => {
|
||||
const wildcardPrefixes = matcher
|
||||
.filter((entry) => entry.endsWith(":path*"))
|
||||
.map((entry) => entry.replace(":path*", ""));
|
||||
|
||||
for (const route of POST_METHODS_ALLOWED_API_ROUTES) {
|
||||
const matcherEntry = route.endsWith("/") ? `${route}:path*` : route;
|
||||
const coveredByWildcard = wildcardPrefixes.some((prefix) => route.startsWith(prefix));
|
||||
const coveredByExact = matcher.includes(matcherEntry);
|
||||
expect(
|
||||
coveredByWildcard || coveredByExact,
|
||||
`POST route "${route}" is not covered by any matcher entry`
|
||||
).toBe(true);
|
||||
}
|
||||
it("should not contain any /api/ routes except /api/auth/signup", () => {
|
||||
const apiRoutes = matcher.filter((entry) => entry.startsWith("/api/") && entry !== "/api/auth/signup");
|
||||
expect(apiRoutes).toEqual([]);
|
||||
});
|
||||
|
||||
it("should not contain API matcher entries without corresponding POST_METHODS_ALLOWED_API_ROUTES routes", () => {
|
||||
const NON_API_ROUTES = ["/auth/login", "/login", "/apps/installed", "/auth/logout", "/:path*/embed"];
|
||||
const apiMatcherEntries = matcher.filter((entry) => !NON_API_ROUTES.includes(entry));
|
||||
|
||||
for (const entry of apiMatcherEntries) {
|
||||
if (entry.endsWith(":path*")) {
|
||||
const prefix = entry.replace(":path*", "");
|
||||
const hasCoveredRoutes = POST_METHODS_ALLOWED_API_ROUTES.some((route) => route.startsWith(prefix));
|
||||
expect(hasCoveredRoutes, `Matcher wildcard "${entry}" doesn't cover any POST route`).toBe(true);
|
||||
} else {
|
||||
expect(
|
||||
POST_METHODS_ALLOWED_API_ROUTES,
|
||||
`Matcher has "${entry}" but it's missing from POST_METHODS_ALLOWED_API_ROUTES`
|
||||
).toContain(entry);
|
||||
}
|
||||
}
|
||||
it("should only contain the expected reduced route set", () => {
|
||||
expect(matcher).toEqual([
|
||||
"/auth/login",
|
||||
"/login",
|
||||
"/apps/installed",
|
||||
"/auth/logout",
|
||||
"/:path*/embed",
|
||||
"/availability",
|
||||
"/api/auth/signup",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-113
@@ -12,71 +12,6 @@ const safeGet = async <T = any>(key: string): Promise<T | undefined> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const POST_METHODS_ALLOWED_API_ROUTES = [
|
||||
"/api/auth/forgot-password",
|
||||
"/api/auth/oauth/me",
|
||||
"/api/auth/oauth/refreshToken",
|
||||
"/api/auth/oauth/token",
|
||||
"/api/auth/reset-password",
|
||||
"/api/auth/saml/callback",
|
||||
"/api/auth/saml/token",
|
||||
"/api/auth/setup",
|
||||
"/api/auth/signup",
|
||||
"/api/auth/two-factor/totp/disable",
|
||||
"/api/auth/two-factor/totp/enable",
|
||||
"/api/auth/two-factor/totp/setup",
|
||||
"/api/auth/session",
|
||||
"/api/availability/calendar",
|
||||
"/api/cancel",
|
||||
"/api/cron/bookingReminder",
|
||||
"/api/cron/calendar-cache-cleanup",
|
||||
"/api/cron/changeTimeZone",
|
||||
"/api/cron/checkSmsPrices",
|
||||
"/api/cron/downgradeUsers",
|
||||
"/api/cron/monthlyDigestEmail",
|
||||
"/api/cron/syncAppMeta",
|
||||
"/api/cron/webhookTriggers",
|
||||
"/api/cron/workflows/scheduleEmailReminders",
|
||||
"/api/cron/workflows/scheduleSMSReminders",
|
||||
"/api/cron/workflows/scheduleWhatsappReminders",
|
||||
"/api/get-inbound-dynamic-variables",
|
||||
"/api/integrations/", // for /api/integrations/[...args] and webhooks
|
||||
"/api/recorded-daily-video",
|
||||
"/api/router",
|
||||
"/api/routing-forms/queued-response",
|
||||
"/api/scim/v2.0/", // /api/scim/v2.0/[...directory]
|
||||
"/api/support/conversation",
|
||||
"/api/sync/helpscout",
|
||||
"/api/twilio/webhook",
|
||||
"/api/username",
|
||||
"/api/verify-booking-token",
|
||||
"/api/video/guest-session",
|
||||
"/api/webhook/app-credential",
|
||||
"/api/webhooks/calendar-subscription/", // /api/webhooks/calendar-subscription/[provider]
|
||||
"/api/webhooks/retell-ai",
|
||||
"/api/workflows/sms/user-response",
|
||||
"/api/trpc/", // for tRPC
|
||||
"/api/auth/callback/", // for NextAuth
|
||||
"/api/book/event",
|
||||
"/api/book/instant-event",
|
||||
"/api/book/recurring-event",
|
||||
"/availability",
|
||||
];
|
||||
|
||||
export function checkPostMethod(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
if (!POST_METHODS_ALLOWED_API_ROUTES.some((route) => pathname.startsWith(route)) && req.method === "POST") {
|
||||
return new NextResponse(null, {
|
||||
status: 405,
|
||||
statusText: "Method Not Allowed",
|
||||
headers: {
|
||||
Allow: "GET",
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Vercel/Edge rejects non‑ASCII header values (see: https://github.com/vercel/next.js/issues/85631)
|
||||
const isAscii = (s: string) => {
|
||||
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) > 0x7f) return false;
|
||||
@@ -131,9 +66,6 @@ const shouldEnforceCsp = (url: URL) => {
|
||||
};
|
||||
|
||||
const proxy = async (req: NextRequest): Promise<NextResponse<unknown>> => {
|
||||
const postCheckResult = checkPostMethod(req);
|
||||
if (postCheckResult) return postCheckResult;
|
||||
|
||||
const url = req.nextUrl;
|
||||
const reqWithEnrichedHeaders = enrichRequestWithHeaders({ req });
|
||||
const requestHeaders = new Headers(reqWithEnrichedHeaders.headers);
|
||||
@@ -231,51 +163,7 @@ function enrichRequestWithHeaders({ req }: { req: NextRequest }) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/auth/login",
|
||||
"/login",
|
||||
"/apps/installed",
|
||||
"/auth/logout",
|
||||
"/:path*/embed",
|
||||
"/api/auth/forgot-password",
|
||||
"/api/auth/oauth/me",
|
||||
"/api/auth/oauth/refreshToken",
|
||||
"/api/auth/oauth/token",
|
||||
"/api/auth/reset-password",
|
||||
"/api/auth/saml/callback",
|
||||
"/api/auth/saml/token",
|
||||
"/api/auth/setup",
|
||||
"/api/auth/signup",
|
||||
"/api/auth/two-factor/totp/disable",
|
||||
"/api/auth/two-factor/totp/enable",
|
||||
"/api/auth/two-factor/totp/setup",
|
||||
"/api/auth/session",
|
||||
"/api/availability/calendar",
|
||||
"/api/cancel",
|
||||
"/api/cron/:path*",
|
||||
"/api/get-inbound-dynamic-variables",
|
||||
"/api/integrations/:path*",
|
||||
"/api/recorded-daily-video",
|
||||
"/api/router",
|
||||
"/api/routing-forms/queued-response",
|
||||
"/api/scim/v2.0/:path*",
|
||||
"/api/support/conversation",
|
||||
"/api/sync/helpscout",
|
||||
"/api/twilio/webhook",
|
||||
"/api/username",
|
||||
"/api/verify-booking-token",
|
||||
"/api/video/guest-session",
|
||||
"/api/webhook/app-credential",
|
||||
"/api/webhooks/calendar-subscription/:path*",
|
||||
"/api/webhooks/retell-ai",
|
||||
"/api/workflows/sms/user-response",
|
||||
"/api/trpc/:path*",
|
||||
"/api/auth/callback/:path*",
|
||||
"/api/book/event",
|
||||
"/api/book/instant-event",
|
||||
"/api/book/recurring-event",
|
||||
"/availability",
|
||||
],
|
||||
matcher: ["/auth/login", "/login", "/apps/installed", "/auth/logout", "/:path*/embed", "/availability", "/api/auth/signup"],
|
||||
};
|
||||
|
||||
export default proxy;
|
||||
|
||||
Reference in New Issue
Block a user