Skip to content

Commit d8a0ea2

Browse files
committed
admin route reconciliation
1 parent 7da5b84 commit d8a0ea2

28 files changed

Lines changed: 1978 additions & 269 deletions

File tree

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

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import type React from 'react'
44
import { createContext, useCallback, useEffect, useMemo, useState } from 'react'
5+
import { createLogger } from '@sim/logger'
56
import { useQueryClient } from '@tanstack/react-query'
67
import { client } from '@/lib/auth/auth-client'
78
import { extractSessionDataFromAuthClientResult } from '@/lib/auth/session-response'
@@ -34,6 +35,8 @@ export type SessionHookResult = {
3435

3536
export const SessionContext = createContext<SessionHookResult | null>(null)
3637

38+
const logger = createLogger('SessionProvider')
39+
3740
export function SessionProvider({ children }: { children: React.ReactNode }) {
3841
const [data, setData] = useState<AppSession>(null)
3942
const [isPending, setIsPending] = useState(true)
@@ -49,14 +52,18 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
4952
: await client.getSession()
5053
const session = extractSessionDataFromAuthClientResult(res) as AppSession
5154
setData(session)
55+
return session
5256
} catch (e) {
5357
setError(e instanceof Error ? e : new Error('Failed to fetch session'))
58+
return null
5459
} finally {
5560
setIsPending(false)
5661
}
5762
}, [])
5863

5964
useEffect(() => {
65+
let isCancelled = false
66+
6067
// Check if user was redirected after plan upgrade
6168
const params = new URLSearchParams(window.location.search)
6269
const wasUpgraded = params.get('upgraded') === 'true'
@@ -69,12 +76,51 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
6976
window.history.replaceState({}, '', newUrl)
7077
}
7178

72-
loadSession(wasUpgraded).then(() => {
73-
if (wasUpgraded) {
74-
queryClient.invalidateQueries({ queryKey: ['organizations'] })
75-
queryClient.invalidateQueries({ queryKey: ['subscription'] })
79+
const initializeSession = async () => {
80+
const session = await loadSession(wasUpgraded)
81+
82+
if (!wasUpgraded || isCancelled) {
83+
return
7684
}
77-
})
85+
86+
queryClient.invalidateQueries({ queryKey: ['organizations'] })
87+
queryClient.invalidateQueries({ queryKey: ['subscription'] })
88+
89+
const activeOrganizationId = session?.session?.activeOrganizationId ?? null
90+
if (activeOrganizationId) {
91+
return
92+
}
93+
94+
try {
95+
const response = await fetch('/api/organizations')
96+
if (!response.ok) {
97+
return
98+
}
99+
100+
const orgData = (await response.json()) as {
101+
organizations?: Array<{ id: string }>
102+
}
103+
const organizationId = orgData.organizations?.[0]?.id
104+
105+
if (!organizationId || isCancelled) {
106+
return
107+
}
108+
109+
await client.organization.setActive({ organizationId })
110+
111+
if (!isCancelled) {
112+
await loadSession(true)
113+
}
114+
} catch (error) {
115+
logger.warn('Failed to activate organization after subscription upgrade', { error })
116+
}
117+
}
118+
119+
void initializeSession()
120+
121+
return () => {
122+
isCancelled = true
123+
}
78124
}, [loadSession, queryClient])
79125

80126
useEffect(() => {
@@ -100,9 +146,13 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
100146
.catch(() => {})
101147
}, [data, isPending])
102148

149+
const refetch = useCallback(async () => {
150+
await loadSession()
151+
}, [loadSession])
152+
103153
const value = useMemo<SessionHookResult>(
104-
() => ({ data, isPending, error, refetch: loadSession }),
105-
[data, isPending, error, loadSession]
154+
() => ({ data, isPending, error, refetch }),
155+
[data, isPending, error, refetch]
106156
)
107157

108158
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ vi.mock('@/lib/core/config/feature-flags', () => ({
3939
},
4040
}))
4141

42-
import { GET } from '@/app/api/auth/[...all]/route'
42+
import { GET, POST } from '@/app/api/auth/[...all]/route'
4343

4444
describe('auth catch-all route (DISABLE_AUTH get-session)', () => {
4545
beforeEach(() => {
@@ -95,3 +95,49 @@ describe('auth catch-all route (DISABLE_AUTH get-session)', () => {
9595
expect(json).toEqual({ data: { ok: true } })
9696
})
9797
})
98+
99+
describe('auth catch-all route organization mutations', () => {
100+
beforeEach(() => {
101+
vi.clearAllMocks()
102+
})
103+
104+
it('blocks Better Auth organization mutation endpoints that bypass app lifecycle rules', async () => {
105+
const req = createMockRequest(
106+
'POST',
107+
undefined,
108+
{},
109+
'http://localhost:3000/api/auth/organization/create'
110+
)
111+
112+
const res = await POST(req as any)
113+
const json = await res.json()
114+
115+
expect(res.status).toBe(404)
116+
expect(handlerMocks.betterAuthPOST).not.toHaveBeenCalled()
117+
expect(json).toEqual({
118+
error: 'Organization mutations are handled by application API routes.',
119+
})
120+
})
121+
122+
it('allows safe Better Auth organization session endpoints', async () => {
123+
const { NextResponse } = await import('next/server')
124+
handlerMocks.betterAuthPOST.mockResolvedValueOnce(
125+
new NextResponse(JSON.stringify({ data: { ok: true } }), {
126+
headers: { 'content-type': 'application/json' },
127+
}) as any
128+
)
129+
130+
const req = createMockRequest(
131+
'POST',
132+
undefined,
133+
{},
134+
'http://localhost:3000/api/auth/organization/set-active'
135+
)
136+
137+
const res = await POST(req as any)
138+
const json = await res.json()
139+
140+
expect(handlerMocks.betterAuthPOST).toHaveBeenCalledTimes(1)
141+
expect(json).toEqual({ data: { ok: true } })
142+
})
143+
})

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import { isAuthDisabled } from '@/lib/core/config/feature-flags'
77
export const dynamic = 'force-dynamic'
88

99
const { GET: betterAuthGET, POST: betterAuthPOST } = toNextJsHandler(auth.handler)
10+
const SAFE_ORGANIZATION_POST_PATHS = new Set(['organization/check-slug', 'organization/set-active'])
11+
12+
function isBlockedOrganizationMutationPath(path: string): boolean {
13+
return path.startsWith('organization/') && !SAFE_ORGANIZATION_POST_PATHS.has(path)
14+
}
1015

1116
export async function GET(request: NextRequest) {
1217
const url = new URL(request.url)
@@ -20,4 +25,16 @@ export async function GET(request: NextRequest) {
2025
return betterAuthGET(request)
2126
}
2227

23-
export const POST = betterAuthPOST
28+
export async function POST(request: NextRequest) {
29+
const url = new URL(request.url)
30+
const path = url.pathname.replace('/api/auth/', '')
31+
32+
if (isBlockedOrganizationMutationPath(path)) {
33+
return NextResponse.json(
34+
{ error: 'Organization mutations are handled by application API routes.' },
35+
{ status: 404 }
36+
)
37+
}
38+
39+
return betterAuthPOST(request)
40+
}

0 commit comments

Comments
 (0)