Skip to content

Commit 0b72227

Browse files
fix(tables): resolve the lock flag with the same context on both gates
The page passed userId/orgId while the PATCH gate resolved the flag with no context, so an org- or user-targeted rollout would show the settings panel and then 403 on save — the rollout path the previous commit exists to enable. Both now key on the workspace's host organization rather than the viewer's active one, matching the convention getWorkspaceHostContextForViewer documents: active-org describes the account, not the workspace host. The route's lookup runs only when a lock is actually being turned on.
1 parent f978389 commit 0b72227

2 files changed

Lines changed: 31 additions & 9 deletions

File tree

  • apps/sim/app

apps/sim/app/api/table/[tableId]/route.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '@/lib/table'
1919
import { getWorkspaceTableLimits } from '@/lib/table/billing'
2020
import { TABLE_LOCK_FLAGS, TABLE_LOCK_KINDS } from '@/lib/table/types'
21+
import { getWorkspaceWithOwner } from '@/lib/workspaces/permissions/utils'
2122
import {
2223
accessError,
2324
checkAccess,
@@ -154,8 +155,20 @@ export const PATCH = withRouteHandler(
154155
const flag = TABLE_LOCK_FLAGS[kind]
155156
return validated.locks?.[flag] === true && !table.locks[flag]
156157
})
157-
if (enablesALock && !(await isFeatureEnabled('table-locks'))) {
158-
return NextResponse.json({ error: 'Table locks are not enabled' }, { status: 403 })
158+
if (enablesALock) {
159+
// Resolve with the same context the page uses to decide whether to
160+
// show the panel — keyed on the workspace's host organization, not
161+
// the viewer's active one. Without it an org- or user-targeted
162+
// rollout would open the panel and then 403 on save. Looked up only
163+
// on the enabling path, so an unlock never pays for it.
164+
const workspace = await getWorkspaceWithOwner(table.workspaceId)
165+
const enabled = await isFeatureEnabled('table-locks', {
166+
userId: authResult.userId,
167+
orgId: workspace?.organizationId ?? undefined,
168+
})
169+
if (!enabled) {
170+
return NextResponse.json({ error: 'Table locks are not enabled' }, { status: 403 })
171+
}
159172
}
160173
const adminResult = await checkAccess(tableId, authResult.userId, 'admin')
161174
if (!adminResult.ok) {

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/page.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,39 @@
11
import { Suspense } from 'react'
22
import type { Metadata } from 'next'
33
import { getSession } from '@/lib/auth'
4-
import { getActiveOrganizationId } from '@/lib/auth/session-response'
54
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
5+
import { getWorkspaceHostContextForViewer } from '@/lib/workspaces/host-context'
66
import TableLoading from '@/app/workspace/[workspaceId]/tables/[tableId]/loading'
77
import { Table } from './table'
88

99
export const metadata: Metadata = {
1010
title: 'Table',
1111
}
1212

13+
interface TablePageProps {
14+
params: Promise<{ workspaceId: string }>
15+
}
16+
1317
/**
1418
* Table-detail page entry. `Table` reads URL query params via nuqs (which uses
1519
* `useSearchParams` internally), so it must sit under a Suspense boundary. The
1620
* fallback renders the real chrome so a suspend never shows a blank frame.
1721
*
1822
* The lock flag is resolved here rather than mirrored into a `NEXT_PUBLIC_` var:
1923
* gating lives in AppConfig, which has no client counterpart, so resolving it
20-
* server-side is the only way its org/user clauses reach the UI. `getSession`
21-
* is request-cached, so this reuses the layout's read.
24+
* server-side is the only way its org/user clauses reach the UI. The org is the
25+
* workspace's host organization, not the viewer's active one — the same key the
26+
* PATCH gate uses, so the panel can't open onto a Save that 403s. Both
27+
* `getSession` and the host context are request-memoized, so this reuses the
28+
* layout's reads.
2229
*/
23-
export default async function TablePage() {
24-
const session = await getSession()
30+
export default async function TablePage({ params }: TablePageProps) {
31+
const [{ workspaceId }, session] = await Promise.all([params, getSession()])
32+
const userId = session?.user?.id
33+
const host = userId ? await getWorkspaceHostContextForViewer(workspaceId, userId) : null
2534
const tableLocksEnabled = await isFeatureEnabled('table-locks', {
26-
userId: session?.user?.id,
27-
orgId: getActiveOrganizationId(session) ?? undefined,
35+
userId,
36+
orgId: host?.hostOrganizationId ?? undefined,
2837
})
2938

3039
return (

0 commit comments

Comments
 (0)