Skip to content

Commit ee579b5

Browse files
committed
fix(auth): make DISABLE_AUTH work in web app
Return an anonymous session using the same response envelope as Better Auth's get-session endpoint, and make the session provider tolerant to both wrapped and raw session payloads. Fixes #2524
1 parent 4913799 commit ee579b5

File tree

6 files changed

+152
-3
lines changed

6 files changed

+152
-3
lines changed

apps/sim/app/_shell/providers/session-provider.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { createContext, useCallback, useEffect, useMemo, useState } from 'react'
55
import { useQueryClient } from '@tanstack/react-query'
66
import posthog from 'posthog-js'
77
import { client } from '@/lib/auth/auth-client'
8+
import { extractSessionDataFromAuthClientResult } from '@/lib/auth/session-response'
89

910
export type AppSession = {
1011
user: {
@@ -45,7 +46,8 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
4546
const res = bypassCache
4647
? await client.getSession({ query: { disableCookieCache: true } })
4748
: await client.getSession()
48-
setData(res?.data ?? null)
49+
const session = extractSessionDataFromAuthClientResult(res) as AppSession
50+
setData(session)
4951
} catch (e) {
5052
setError(e instanceof Error ? e : new Error('Failed to fetch session'))
5153
} finally {
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { createMockRequest, setupCommonApiMocks } from '@sim/testing'
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
const handlerMocks = vi.hoisted(() => ({
8+
betterAuthGET: vi.fn(),
9+
betterAuthPOST: vi.fn(),
10+
ensureAnonymousUserExists: vi.fn(),
11+
createAnonymousGetSessionResponse: vi.fn(() => ({
12+
data: {
13+
user: { id: 'anon' },
14+
session: { id: 'anon-session' },
15+
},
16+
})),
17+
}))
18+
19+
vi.mock('better-auth/next-js', () => ({
20+
toNextJsHandler: () => ({
21+
GET: handlerMocks.betterAuthGET,
22+
POST: handlerMocks.betterAuthPOST,
23+
}),
24+
}))
25+
26+
vi.mock('@/lib/auth', () => ({
27+
auth: { handler: {} },
28+
}))
29+
30+
vi.mock('@/lib/auth/anonymous', () => ({
31+
ensureAnonymousUserExists: handlerMocks.ensureAnonymousUserExists,
32+
createAnonymousGetSessionResponse: handlerMocks.createAnonymousGetSessionResponse,
33+
}))
34+
35+
describe('auth catch-all route (DISABLE_AUTH get-session)', () => {
36+
beforeEach(() => {
37+
vi.resetModules()
38+
setupCommonApiMocks()
39+
handlerMocks.betterAuthGET.mockReset()
40+
handlerMocks.betterAuthPOST.mockReset()
41+
handlerMocks.ensureAnonymousUserExists.mockReset()
42+
handlerMocks.createAnonymousGetSessionResponse.mockClear()
43+
})
44+
45+
it('returns anonymous session in better-auth response envelope when auth is disabled', async () => {
46+
vi.doMock('@/lib/core/config/feature-flags', () => ({ isAuthDisabled: true }))
47+
48+
const req = createMockRequest(
49+
'GET',
50+
undefined,
51+
{},
52+
'http://localhost:3000/api/auth/get-session'
53+
)
54+
const { GET } = await import('@/app/api/auth/[...all]/route')
55+
56+
const res = await GET(req as any)
57+
const json = await res.json()
58+
59+
expect(handlerMocks.ensureAnonymousUserExists).toHaveBeenCalledTimes(1)
60+
expect(handlerMocks.betterAuthGET).not.toHaveBeenCalled()
61+
expect(json).toEqual({
62+
data: {
63+
user: { id: 'anon' },
64+
session: { id: 'anon-session' },
65+
},
66+
})
67+
})
68+
69+
it('delegates to better-auth handler when auth is enabled', async () => {
70+
vi.doMock('@/lib/core/config/feature-flags', () => ({ isAuthDisabled: false }))
71+
72+
handlerMocks.betterAuthGET.mockResolvedValueOnce(
73+
new (await import('next/server')).NextResponse(JSON.stringify({ data: { ok: true } }), {
74+
headers: { 'content-type': 'application/json' },
75+
}) as any
76+
)
77+
78+
const req = createMockRequest(
79+
'GET',
80+
undefined,
81+
{},
82+
'http://localhost:3000/api/auth/get-session'
83+
)
84+
const { GET } = await import('@/app/api/auth/[...all]/route')
85+
86+
const res = await GET(req as any)
87+
const json = await res.json()
88+
89+
expect(handlerMocks.ensureAnonymousUserExists).not.toHaveBeenCalled()
90+
expect(handlerMocks.betterAuthGET).toHaveBeenCalledTimes(1)
91+
expect(json).toEqual({ data: { ok: true } })
92+
})
93+
})

apps/sim/app/api/auth/[...all]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { toNextJsHandler } from 'better-auth/next-js'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { auth } from '@/lib/auth'
4-
import { createAnonymousSession, ensureAnonymousUserExists } from '@/lib/auth/anonymous'
4+
import { createAnonymousGetSessionResponse, ensureAnonymousUserExists } from '@/lib/auth/anonymous'
55
import { isAuthDisabled } from '@/lib/core/config/feature-flags'
66

77
export const dynamic = 'force-dynamic'
@@ -14,7 +14,7 @@ export async function GET(request: NextRequest) {
1414

1515
if (path === 'get-session' && isAuthDisabled) {
1616
await ensureAnonymousUserExists()
17-
return NextResponse.json(createAnonymousSession())
17+
return NextResponse.json(createAnonymousGetSessionResponse())
1818
}
1919

2020
return betterAuthGET(request)

apps/sim/lib/auth/anonymous.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,7 @@ export function createAnonymousSession(): AnonymousSession {
102102
},
103103
}
104104
}
105+
106+
export function createAnonymousGetSessionResponse(): { data: AnonymousSession } {
107+
return { data: createAnonymousSession() }
108+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { extractSessionDataFromAuthClientResult } from '@/lib/auth/session-response'
6+
7+
describe('extractSessionDataFromAuthClientResult', () => {
8+
it('returns null for non-objects', () => {
9+
expect(extractSessionDataFromAuthClientResult(null)).toBeNull()
10+
expect(extractSessionDataFromAuthClientResult(undefined)).toBeNull()
11+
expect(extractSessionDataFromAuthClientResult('nope')).toBeNull()
12+
expect(extractSessionDataFromAuthClientResult(123)).toBeNull()
13+
})
14+
15+
it('prefers .data when present', () => {
16+
expect(extractSessionDataFromAuthClientResult({ data: null })).toBeNull()
17+
18+
const session = { user: { id: 'u1' }, session: { id: 's1' } }
19+
expect(extractSessionDataFromAuthClientResult({ data: session })).toEqual(session)
20+
})
21+
22+
it('falls back to raw session payload shape', () => {
23+
const raw = { user: { id: 'u1' }, session: { id: 's1' } }
24+
expect(extractSessionDataFromAuthClientResult(raw)).toEqual(raw)
25+
})
26+
27+
it('returns null for unknown object shapes', () => {
28+
expect(extractSessionDataFromAuthClientResult({})).toBeNull()
29+
expect(extractSessionDataFromAuthClientResult({ ok: true })).toBeNull()
30+
})
31+
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export function extractSessionDataFromAuthClientResult(result: unknown): unknown | null {
2+
if (!result || typeof result !== 'object') {
3+
return null
4+
}
5+
6+
const record = result as Record<string, unknown>
7+
8+
// Expected shape from better-auth client: { data: <session> }
9+
if ('data' in record) {
10+
return (record as { data?: unknown }).data ?? null
11+
}
12+
13+
// Fallback for raw session payloads: { user, session }
14+
if ('user' in record) {
15+
return record
16+
}
17+
18+
return null
19+
}

0 commit comments

Comments
 (0)