Skip to content

Commit 8a3944f

Browse files
keithwillcodecubic-dev-ai[bot]devin-ai-integration[bot]
authored
chore: Remove /api/ routes from proxy (calcom#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 calcom#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>
1 parent be0b707 commit 8a3944f

2 files changed

Lines changed: 19 additions & 174 deletions

File tree

apps/web/proxy.test.ts

Lines changed: 18 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { NextRequest, NextResponse } from "next/server";
66
import type { Mock } from "vitest";
77
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
88
// We'll test the wrapped proxy as it would be used in production
9-
import proxy, { checkPostMethod, POST_METHODS_ALLOWED_API_ROUTES } from "./proxy";
9+
import proxy from "./proxy";
1010
import { config } from "./proxy";
1111

1212
// Mock dependencies at module level
@@ -146,34 +146,6 @@ const callProxy = async (req: NextRequest): Promise<Response> => {
146146
return (await (proxy as any)(req)) as Response;
147147
};
148148

149-
describe("Middleware - POST requests restriction", () => {
150-
const createRequest = (path: string, method: string) => {
151-
return new NextRequest(
152-
new Request(`${WEBAPP_URL}${path}`, {
153-
method,
154-
})
155-
);
156-
};
157-
158-
it("should allow POST requests to /api routes", async () => {
159-
const req1 = createRequest("/api/auth/signup", "POST");
160-
const res1 = checkPostMethod(req1);
161-
expect(res1).toBeNull();
162-
});
163-
164-
it("should allow GET requests to app routes", async () => {
165-
const req = createRequest("/team/xyz", "GET");
166-
const res = checkPostMethod(req);
167-
expect(res).toBeNull();
168-
});
169-
170-
it("should allow GET requests to /api routes", async () => {
171-
const req = createRequest("/api/auth/signup", "GET");
172-
const res = checkPostMethod(req);
173-
expect(res).toBeNull();
174-
});
175-
});
176-
177149
describe("Middleware Integration Tests", () => {
178150
beforeEach(() => {
179151
vi.clearAllMocks();
@@ -486,9 +458,11 @@ describe("Middleware Matcher Configuration", () => {
486458

487459
it("should include all core middleware routes", () => {
488460
expect(matcher).toContain("/auth/login");
489-
expect(matcher).toContain("/login");
490-
expect(matcher).toContain("/apps/installed");
491461
expect(matcher).toContain("/auth/logout");
462+
expect(matcher).toContain("/api/auth/signup");
463+
expect(matcher).toContain("/apps/installed");
464+
expect(matcher).toContain("/availability");
465+
expect(matcher).toContain("/login");
492466
expect(matcher).toContain("/:path*/embed");
493467
});
494468

@@ -497,37 +471,20 @@ describe("Middleware Matcher Configuration", () => {
497471
expect(uniqueEntries.size).toBe(matcher.length);
498472
});
499473

500-
it("should cover every POST_METHODS_ALLOWED_API_ROUTES route via exact match or wildcard", () => {
501-
const wildcardPrefixes = matcher
502-
.filter((entry) => entry.endsWith(":path*"))
503-
.map((entry) => entry.replace(":path*", ""));
504-
505-
for (const route of POST_METHODS_ALLOWED_API_ROUTES) {
506-
const matcherEntry = route.endsWith("/") ? `${route}:path*` : route;
507-
const coveredByWildcard = wildcardPrefixes.some((prefix) => route.startsWith(prefix));
508-
const coveredByExact = matcher.includes(matcherEntry);
509-
expect(
510-
coveredByWildcard || coveredByExact,
511-
`POST route "${route}" is not covered by any matcher entry`
512-
).toBe(true);
513-
}
474+
it("should not contain any /api/ routes except /api/auth/signup", () => {
475+
const apiRoutes = matcher.filter((entry) => entry.startsWith("/api/") && entry !== "/api/auth/signup");
476+
expect(apiRoutes).toEqual([]);
514477
});
515478

516-
it("should not contain API matcher entries without corresponding POST_METHODS_ALLOWED_API_ROUTES routes", () => {
517-
const NON_API_ROUTES = ["/auth/login", "/login", "/apps/installed", "/auth/logout", "/:path*/embed"];
518-
const apiMatcherEntries = matcher.filter((entry) => !NON_API_ROUTES.includes(entry));
519-
520-
for (const entry of apiMatcherEntries) {
521-
if (entry.endsWith(":path*")) {
522-
const prefix = entry.replace(":path*", "");
523-
const hasCoveredRoutes = POST_METHODS_ALLOWED_API_ROUTES.some((route) => route.startsWith(prefix));
524-
expect(hasCoveredRoutes, `Matcher wildcard "${entry}" doesn't cover any POST route`).toBe(true);
525-
} else {
526-
expect(
527-
POST_METHODS_ALLOWED_API_ROUTES,
528-
`Matcher has "${entry}" but it's missing from POST_METHODS_ALLOWED_API_ROUTES`
529-
).toContain(entry);
530-
}
531-
}
479+
it("should only contain the expected reduced route set", () => {
480+
expect(matcher).toEqual([
481+
"/auth/login",
482+
"/login",
483+
"/apps/installed",
484+
"/auth/logout",
485+
"/:path*/embed",
486+
"/availability",
487+
"/api/auth/signup",
488+
]);
532489
});
533490
});

apps/web/proxy.ts

Lines changed: 1 addition & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -12,71 +12,6 @@ const safeGet = async <T = any>(key: string): Promise<T | undefined> => {
1212
}
1313
};
1414

15-
export const POST_METHODS_ALLOWED_API_ROUTES = [
16-
"/api/auth/forgot-password",
17-
"/api/auth/oauth/me",
18-
"/api/auth/oauth/refreshToken",
19-
"/api/auth/oauth/token",
20-
"/api/auth/reset-password",
21-
"/api/auth/saml/callback",
22-
"/api/auth/saml/token",
23-
"/api/auth/setup",
24-
"/api/auth/signup",
25-
"/api/auth/two-factor/totp/disable",
26-
"/api/auth/two-factor/totp/enable",
27-
"/api/auth/two-factor/totp/setup",
28-
"/api/auth/session",
29-
"/api/availability/calendar",
30-
"/api/cancel",
31-
"/api/cron/bookingReminder",
32-
"/api/cron/calendar-cache-cleanup",
33-
"/api/cron/changeTimeZone",
34-
"/api/cron/checkSmsPrices",
35-
"/api/cron/downgradeUsers",
36-
"/api/cron/monthlyDigestEmail",
37-
"/api/cron/syncAppMeta",
38-
"/api/cron/webhookTriggers",
39-
"/api/cron/workflows/scheduleEmailReminders",
40-
"/api/cron/workflows/scheduleSMSReminders",
41-
"/api/cron/workflows/scheduleWhatsappReminders",
42-
"/api/get-inbound-dynamic-variables",
43-
"/api/integrations/", // for /api/integrations/[...args] and webhooks
44-
"/api/recorded-daily-video",
45-
"/api/router",
46-
"/api/routing-forms/queued-response",
47-
"/api/scim/v2.0/", // /api/scim/v2.0/[...directory]
48-
"/api/support/conversation",
49-
"/api/sync/helpscout",
50-
"/api/twilio/webhook",
51-
"/api/username",
52-
"/api/verify-booking-token",
53-
"/api/video/guest-session",
54-
"/api/webhook/app-credential",
55-
"/api/webhooks/calendar-subscription/", // /api/webhooks/calendar-subscription/[provider]
56-
"/api/webhooks/retell-ai",
57-
"/api/workflows/sms/user-response",
58-
"/api/trpc/", // for tRPC
59-
"/api/auth/callback/", // for NextAuth
60-
"/api/book/event",
61-
"/api/book/instant-event",
62-
"/api/book/recurring-event",
63-
"/availability",
64-
];
65-
66-
export function checkPostMethod(req: NextRequest) {
67-
const pathname = req.nextUrl.pathname;
68-
if (!POST_METHODS_ALLOWED_API_ROUTES.some((route) => pathname.startsWith(route)) && req.method === "POST") {
69-
return new NextResponse(null, {
70-
status: 405,
71-
statusText: "Method Not Allowed",
72-
headers: {
73-
Allow: "GET",
74-
},
75-
});
76-
}
77-
return null;
78-
}
79-
8015
// Vercel/Edge rejects non‑ASCII header values (see: https://github.com/vercel/next.js/issues/85631)
8116
const isAscii = (s: string) => {
8217
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) > 0x7f) return false;
@@ -131,9 +66,6 @@ const shouldEnforceCsp = (url: URL) => {
13166
};
13267

13368
const proxy = async (req: NextRequest): Promise<NextResponse<unknown>> => {
134-
const postCheckResult = checkPostMethod(req);
135-
if (postCheckResult) return postCheckResult;
136-
13769
const url = req.nextUrl;
13870
const reqWithEnrichedHeaders = enrichRequestWithHeaders({ req });
13971
const requestHeaders = new Headers(reqWithEnrichedHeaders.headers);
@@ -231,51 +163,7 @@ function enrichRequestWithHeaders({ req }: { req: NextRequest }) {
231163
}
232164

233165
export const config = {
234-
matcher: [
235-
"/auth/login",
236-
"/login",
237-
"/apps/installed",
238-
"/auth/logout",
239-
"/:path*/embed",
240-
"/api/auth/forgot-password",
241-
"/api/auth/oauth/me",
242-
"/api/auth/oauth/refreshToken",
243-
"/api/auth/oauth/token",
244-
"/api/auth/reset-password",
245-
"/api/auth/saml/callback",
246-
"/api/auth/saml/token",
247-
"/api/auth/setup",
248-
"/api/auth/signup",
249-
"/api/auth/two-factor/totp/disable",
250-
"/api/auth/two-factor/totp/enable",
251-
"/api/auth/two-factor/totp/setup",
252-
"/api/auth/session",
253-
"/api/availability/calendar",
254-
"/api/cancel",
255-
"/api/cron/:path*",
256-
"/api/get-inbound-dynamic-variables",
257-
"/api/integrations/:path*",
258-
"/api/recorded-daily-video",
259-
"/api/router",
260-
"/api/routing-forms/queued-response",
261-
"/api/scim/v2.0/:path*",
262-
"/api/support/conversation",
263-
"/api/sync/helpscout",
264-
"/api/twilio/webhook",
265-
"/api/username",
266-
"/api/verify-booking-token",
267-
"/api/video/guest-session",
268-
"/api/webhook/app-credential",
269-
"/api/webhooks/calendar-subscription/:path*",
270-
"/api/webhooks/retell-ai",
271-
"/api/workflows/sms/user-response",
272-
"/api/trpc/:path*",
273-
"/api/auth/callback/:path*",
274-
"/api/book/event",
275-
"/api/book/instant-event",
276-
"/api/book/recurring-event",
277-
"/availability",
278-
],
166+
matcher: ["/auth/login", "/login", "/apps/installed", "/auth/logout", "/:path*/embed", "/availability", "/api/auth/signup"],
279167
};
280168

281169
export default proxy;

0 commit comments

Comments
 (0)