Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions __tests__/authorize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import { authorizeRequest } from "@/lib/auth/authorize";

const url = (path: string, host = "app.router.so") =>
new URL(path, `https://${host}`);

describe("authorizeRequest", () => {
it("redirects signed-out dashboard requests to the sign-in page", () => {
const result = authorizeRequest({
authenticated: false,
nextUrl: url("/endpoints?tab=all"),
});
expect(result).not.toBe(true);
if (result === true) throw new Error("expected a redirect");
expect(result.status).toBe(307);
const location = new URL(result.headers.get("location")!);
expect(location.pathname).toBe("/login");
expect(location.searchParams.get("callbackUrl")).toBe(
"https://app.router.so/endpoints?tab=all"
);
});

it("does not redirect the sign-in page itself", () => {
expect(
authorizeRequest({ authenticated: false, nextUrl: url("/login") })
).toBe(true);
});

it("allows signed-in requests", () => {
expect(
authorizeRequest({ authenticated: true, nextUrl: url("/") })
).toBe(true);
});

it("keeps public form surfaces reachable without a session", () => {
for (const path of [
"/f/abc",
"/embed/v1.js",
"/api/public/forms/abc/definition",
"/api/integrations/wordpress/connect",
]) {
expect(
authorizeRequest({ authenticated: false, nextUrl: url(path) })
).toBe(true);
}
expect(
authorizeRequest({
authenticated: false,
nextUrl: url("/abc", "forms.router.so"),
})
).toBe(true);
});
});
42 changes: 42 additions & 0 deletions lib/auth/authorize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { NextResponse } from "next/server";

export const SIGN_IN_PATH = "/login";

/** Surfaces that must stay reachable without a dashboard session. */
export function isPublicFormSurface(url: {
hostname: string;
pathname: string;
}): boolean {
return (
url.hostname === "forms.router.so" ||
url.pathname.startsWith("/f/") ||
url.pathname.startsWith("/embed/") ||
url.pathname.startsWith("/api/public/") ||
url.pathname.startsWith("/api/integrations/wordpress/")
);
}

/**
* next-auth only redirects unauthenticated requests to the sign-in page on
* its own when no custom middleware function is supplied. middleware.ts wraps
* `auth()` to rewrite the forms host, so the redirect must be returned from
* the `authorized` callback instead; next-auth honours a Response there.
*/
export function authorizeRequest({
authenticated,
nextUrl,
}: {
authenticated: boolean;
nextUrl: URL;
}): true | NextResponse {
if (
authenticated ||
isPublicFormSurface(nextUrl) ||
nextUrl.pathname === SIGN_IN_PATH
) {
return true;
}
const signInUrl = new URL(SIGN_IN_PATH, nextUrl.href);
signInUrl.searchParams.set("callbackUrl", nextUrl.href);
return NextResponse.redirect(signInUrl);
}
14 changes: 3 additions & 11 deletions lib/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { NextAuthConfig } from "next-auth";
import { User } from "next-auth";
import Resend from "next-auth/providers/resend";
import GitHub from "next-auth/providers/github";
import { authorizeRequest } from "./authorize";

declare module "next-auth" {
interface Session extends User {
Expand Down Expand Up @@ -38,17 +39,8 @@ export const config = {
}
return token;
},
authorized: async ({ auth, request }) => {
const hostname = request.nextUrl.hostname;
const pathname = request.nextUrl.pathname;
const isPublicFormSurface =
hostname === "forms.router.so" ||
pathname.startsWith("/f/") ||
pathname.startsWith("/embed/") ||
pathname.startsWith("/api/public/") ||
pathname.startsWith("/api/integrations/wordpress/");
return isPublicFormSurface || !!auth;
},
authorized: async ({ auth, request }) =>
authorizeRequest({ authenticated: !!auth, nextUrl: request.nextUrl }),
},
pages: {
signIn: "/login",
Expand Down
Loading