diff --git a/apps/web/proxy.test.ts b/apps/web/proxy.test.ts index 6a7d65e071..fef56242a8 100644 --- a/apps/web/proxy.test.ts +++ b/apps/web/proxy.test.ts @@ -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 => { 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", + ]); }); }); diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts index ff42ec19aa..f656c7be34 100644 --- a/apps/web/proxy.ts +++ b/apps/web/proxy.ts @@ -12,71 +12,6 @@ const safeGet = async (key: string): Promise => { } }; -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> => { - 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;