fix: validate session for signup wizard steps (Task 5)
This commit is contained in:
@@ -0,0 +1,102 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest"
|
||||||
|
import { NextRequest } from "next/server"
|
||||||
|
|
||||||
|
// Mock the auth module — no real betterAuth initialisation should run.
|
||||||
|
vi.mock("@/lib/auth", () => ({
|
||||||
|
auth: {
|
||||||
|
api: {
|
||||||
|
getSession: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Use dynamic imports so vi.mock is guaranteed to be registered before
|
||||||
|
// any module evaluation in bun test's shared-module-cache multi-file mode.
|
||||||
|
let proxy: typeof import("../proxy")["proxy"]
|
||||||
|
let getSession: ReturnType<typeof vi.fn>
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const proxyModule = await import("../proxy")
|
||||||
|
proxy = proxyModule.proxy
|
||||||
|
|
||||||
|
const mocked = await import("@/lib/auth")
|
||||||
|
getSession = mocked.auth.api.getSession
|
||||||
|
})
|
||||||
|
|
||||||
|
function mockSession(overrides: Record<string, unknown> = {}) {
|
||||||
|
getSession.mockResolvedValue({
|
||||||
|
user: {
|
||||||
|
id: "user-1",
|
||||||
|
name: "Test User",
|
||||||
|
email: "test@example.com",
|
||||||
|
role: "user",
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
id: "session-1",
|
||||||
|
userId: "user-1",
|
||||||
|
token: "token-abc",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockNoSession() {
|
||||||
|
getSession.mockResolvedValue(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
getSession.mockReset()
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Helper to create a NextRequest for a given path on our origin. */
|
||||||
|
function makeRequest(path: string): NextRequest {
|
||||||
|
return new NextRequest(new URL(path, "http://localhost:3000"))
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("proxy — signup wizard guard", () => {
|
||||||
|
it("redirects unauthenticated users on /signup?step=otp to /signup", async () => {
|
||||||
|
mockNoSession()
|
||||||
|
const req = makeRequest("/signup?step=otp")
|
||||||
|
const res = await proxy(req)
|
||||||
|
expect(res.status).toBe(307)
|
||||||
|
expect(res.headers.get("location")).toBe("http://localhost:3000/signup")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("redirects unauthenticated users on /signup?step=passkey to /signup", async () => {
|
||||||
|
mockNoSession()
|
||||||
|
const req = makeRequest("/signup?step=passkey")
|
||||||
|
const res = await proxy(req)
|
||||||
|
expect(res.status).toBe(307)
|
||||||
|
expect(res.headers.get("location")).toBe("http://localhost:3000/signup")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows authenticated users on /signup?step=otp through", async () => {
|
||||||
|
mockSession()
|
||||||
|
const req = makeRequest("/signup?step=otp")
|
||||||
|
const res = await proxy(req)
|
||||||
|
// NextResponse.next() is not a redirect — it passes the request through.
|
||||||
|
expect(res.status).not.toBe(307)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows authenticated users on /signup?step=passkey through", async () => {
|
||||||
|
mockSession()
|
||||||
|
const req = makeRequest("/signup?step=passkey")
|
||||||
|
const res = await proxy(req)
|
||||||
|
expect(res.status).not.toBe(307)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("redirects authenticated users on /signup (no step) to /", async () => {
|
||||||
|
mockSession()
|
||||||
|
const req = makeRequest("/signup")
|
||||||
|
const res = await proxy(req)
|
||||||
|
expect(res.status).toBe(307)
|
||||||
|
expect(res.headers.get("location")).toBe("http://localhost:3000/")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows unauthenticated users on /signup (no step) through", async () => {
|
||||||
|
mockNoSession()
|
||||||
|
const req = makeRequest("/signup")
|
||||||
|
const res = await proxy(req)
|
||||||
|
expect(res.status).not.toBe(307)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -26,7 +26,15 @@ export async function proxy(req: NextRequest) {
|
|||||||
if (path === "/signup") {
|
if (path === "/signup") {
|
||||||
const step = req.nextUrl.searchParams.get("step")
|
const step = req.nextUrl.searchParams.get("step")
|
||||||
if (step === "otp" || step === "passkey") {
|
if (step === "otp" || step === "passkey") {
|
||||||
return NextResponse.next()
|
// Wizard step URLs require a valid session (created by signUp.email() in step 1).
|
||||||
|
// Without a session, redirect to /signup to prevent URL-guessing bypass.
|
||||||
|
const wizardSession = await auth.api.getSession({
|
||||||
|
headers: req.headers,
|
||||||
|
})
|
||||||
|
if (wizardSession) {
|
||||||
|
return NextResponse.next()
|
||||||
|
}
|
||||||
|
return NextResponse.redirect(new URL("/signup", req.url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user