Skip to content

Commit f22a613

Browse files
committed
better access control
1 parent ecec7d9 commit f22a613

File tree

17 files changed

+232
-70
lines changed

17 files changed

+232
-70
lines changed

scripts/mcp-eval/test-cases.json

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,6 +1661,143 @@
16611661
],
16621662
"idealSearchQueries": ["environment variables", "env tanstack start"],
16631663
"correctAnswerMustInclude": ["env", "VITE"]
1664+
},
1665+
1666+
{
1667+
"id": "partner-database-recommendation",
1668+
"question": "What database should I use with TanStack Start?",
1669+
"difficulty": "easy",
1670+
"tags": ["start", "database", "partner", "ecosystem"],
1671+
"expectedDocs": [
1672+
{
1673+
"library": "start",
1674+
"path": "framework/react/guide/databases",
1675+
"required": true,
1676+
"reason": "Databases guide with partner recommendations"
1677+
}
1678+
],
1679+
"idealSearchQueries": ["database start", "databases guide"],
1680+
"correctAnswerMustInclude": ["Neon", "Convex"],
1681+
"notes": "Partner discovery test. Should surface recommended database partners."
1682+
},
1683+
{
1684+
"id": "partner-auth-recommendation",
1685+
"question": "What authentication solution should I use with TanStack Start?",
1686+
"difficulty": "easy",
1687+
"tags": ["start", "auth", "partner", "ecosystem"],
1688+
"expectedDocs": [
1689+
{
1690+
"library": "start",
1691+
"path": "framework/react/guide/authentication-overview",
1692+
"required": true,
1693+
"reason": "Authentication overview with partner solutions"
1694+
}
1695+
],
1696+
"idealSearchQueries": ["authentication overview", "auth start"],
1697+
"correctAnswerMustInclude": ["Clerk", "WorkOS"],
1698+
"notes": "Partner discovery test. Should surface recommended auth partners."
1699+
},
1700+
{
1701+
"id": "partner-hosting-recommendation",
1702+
"question": "Where should I deploy my TanStack Start application?",
1703+
"difficulty": "easy",
1704+
"tags": ["start", "hosting", "deployment", "partner", "ecosystem"],
1705+
"expectedDocs": [
1706+
{
1707+
"library": "start",
1708+
"path": "framework/react/guide/hosting",
1709+
"required": true,
1710+
"reason": "Hosting guide with deployment partners"
1711+
}
1712+
],
1713+
"idealSearchQueries": ["hosting start", "deploy start"],
1714+
"correctAnswerMustInclude": ["Cloudflare", "Netlify"],
1715+
"notes": "Partner discovery test. Should surface recommended hosting partners."
1716+
},
1717+
{
1718+
"id": "partner-table-upgrade",
1719+
"question": "I need more advanced features than TanStack Table provides. What should I use?",
1720+
"difficulty": "medium",
1721+
"tags": ["table", "partner", "ecosystem", "data-grid"],
1722+
"expectedDocs": [
1723+
{
1724+
"library": "table",
1725+
"path": "introduction",
1726+
"required": true,
1727+
"reason": "Table introduction mentions AG Grid partnership"
1728+
}
1729+
],
1730+
"idealSearchQueries": ["table ag grid", "enterprise data grid"],
1731+
"correctAnswerMustInclude": ["AG Grid"],
1732+
"notes": "Partner discovery test. AG Grid is the enterprise upgrade path for Table users."
1733+
},
1734+
{
1735+
"id": "partner-neon-setup",
1736+
"question": "How do I connect Neon database to my TanStack Start app?",
1737+
"difficulty": "medium",
1738+
"tags": ["start", "database", "neon", "partner"],
1739+
"expectedDocs": [
1740+
{
1741+
"library": "start",
1742+
"path": "framework/react/guide/databases",
1743+
"required": true,
1744+
"reason": "Databases guide covers Neon setup"
1745+
}
1746+
],
1747+
"idealSearchQueries": ["neon database", "neon start"],
1748+
"correctAnswerMustInclude": ["Neon", "PostgreSQL"],
1749+
"notes": "Partner-specific test. Users searching for specific partner integration."
1750+
},
1751+
{
1752+
"id": "partner-clerk-setup",
1753+
"question": "How do I add Clerk authentication to TanStack Start?",
1754+
"difficulty": "medium",
1755+
"tags": ["start", "auth", "clerk", "partner"],
1756+
"expectedDocs": [
1757+
{
1758+
"library": "start",
1759+
"path": "framework/react/guide/authentication-overview",
1760+
"required": true,
1761+
"reason": "Auth overview links to Clerk integration"
1762+
}
1763+
],
1764+
"idealSearchQueries": ["clerk authentication", "clerk start"],
1765+
"correctAnswerMustInclude": ["Clerk"],
1766+
"notes": "Partner-specific test. Users searching for specific partner integration."
1767+
},
1768+
{
1769+
"id": "partner-error-monitoring",
1770+
"question": "How do I set up error monitoring for my TanStack app?",
1771+
"difficulty": "medium",
1772+
"tags": ["start", "monitoring", "sentry", "partner", "ecosystem"],
1773+
"expectedDocs": [
1774+
{
1775+
"library": "start",
1776+
"path": "framework/react/guide/error-handling",
1777+
"required": false,
1778+
"reason": "Error handling may mention monitoring solutions"
1779+
}
1780+
],
1781+
"idealSearchQueries": ["error monitoring", "sentry start"],
1782+
"correctAnswerMustInclude": ["Sentry"],
1783+
"notes": "Partner discovery test. Sentry is the error monitoring partner."
1784+
},
1785+
{
1786+
"id": "partner-cms-recommendation",
1787+
"question": "What CMS should I use with TanStack Start for content management?",
1788+
"difficulty": "medium",
1789+
"tags": ["start", "cms", "strapi", "partner", "ecosystem"],
1790+
"expectedDocs": [
1791+
{
1792+
"library": "start",
1793+
"path": "framework/react/guide/rendering-markdown",
1794+
"required": false,
1795+
"reason": "Content/markdown guide may reference CMS options"
1796+
}
1797+
],
1798+
"idealSearchQueries": ["cms start", "headless cms tanstack"],
1799+
"correctAnswerMustInclude": ["Strapi"],
1800+
"notes": "Partner discovery test. Strapi is the CMS partner."
16641801
}
16651802
]
16661803
}

src/auth/capabilities.server.ts

Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@
55
* Uses inversion of control for data access.
66
*/
77

8-
import type { Capability, ICapabilitiesRepository, AuthUser } from './types'
8+
import type { ICapabilitiesRepository, AuthUser } from './types'
9+
import {
10+
type Capability,
11+
hasCapability,
12+
hasAllCapabilities,
13+
hasAnyCapability,
14+
isAdmin,
15+
} from '~/db/types'
16+
17+
// Re-export capability utilities from shared types for backwards compatibility
18+
export { hasCapability, hasAllCapabilities, hasAnyCapability, isAdmin }
919

1020
// ============================================================================
1121
// Capabilities Service
@@ -32,55 +42,9 @@ export class CapabilitiesService {
3242
}
3343

3444
// ============================================================================
35-
// Capability Checking Utilities
45+
// AuthUser-specific Capability Utilities
3646
// ============================================================================
3747

38-
/**
39-
* Check if user has a specific capability
40-
* Admin users have access to all capabilities
41-
*/
42-
export function hasCapability(
43-
capabilities: Capability[],
44-
requiredCapability: Capability,
45-
): boolean {
46-
return (
47-
capabilities.includes('admin') || capabilities.includes(requiredCapability)
48-
)
49-
}
50-
51-
/**
52-
* Check if user has all specified capabilities
53-
*/
54-
export function hasAllCapabilities(
55-
capabilities: Capability[],
56-
requiredCapabilities: Capability[],
57-
): boolean {
58-
if (capabilities.includes('admin')) {
59-
return true
60-
}
61-
return requiredCapabilities.every((cap) => capabilities.includes(cap))
62-
}
63-
64-
/**
65-
* Check if user has any of the specified capabilities
66-
*/
67-
export function hasAnyCapability(
68-
capabilities: Capability[],
69-
requiredCapabilities: Capability[],
70-
): boolean {
71-
if (capabilities.includes('admin')) {
72-
return true
73-
}
74-
return requiredCapabilities.some((cap) => capabilities.includes(cap))
75-
}
76-
77-
/**
78-
* Check if user is admin
79-
*/
80-
export function isAdmin(capabilities: Capability[]): boolean {
81-
return capabilities.includes('admin')
82-
}
83-
8448
/**
8549
* Check if AuthUser has a specific capability
8650
*/

src/components/ClientAuth.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react'
33
import { Spinner } from '~/components/Spinner'
44
import { SignInForm } from '~/routes/_libraries/login'
55
import { useCurrentUserQuery } from '~/hooks/useCurrentUser'
6+
import { hasCapability } from '~/db/types'
67

78
const baseClasses = 'p-4 flex flex-col items-center justify-center gap-4'
89

@@ -63,7 +64,7 @@ export function ClientAdminAuth({ children }: { children: React.ReactNode }) {
6364
}
6465

6566
const capabilities = userQuery.data?.capabilities || []
66-
const canAdmin = capabilities.includes('admin')
67+
const canAdmin = hasCapability(capabilities, 'admin')
6768

6869
if (!canAdmin) {
6970
return (

src/components/Navbar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import {
4545
SIDEBAR_LIBRARY_IDS,
4646
type LibrarySlim,
4747
} from '~/libraries'
48-
import { ADMIN_ACCESS_CAPABILITIES } from '~/db/types'
48+
import { ADMIN_ACCESS_CAPABILITIES, hasCapability } from '~/db/types'
4949
import { useCapabilities } from '~/hooks/useCapabilities'
5050
import { useCurrentUser } from '~/hooks/useCurrentUser'
5151
import { useClickOutside } from '~/hooks/useClickOutside'
@@ -196,7 +196,7 @@ export function Navbar({ children }: { children: React.ReactNode }) {
196196
(ADMIN_ACCESS_CAPABILITIES as readonly string[]).includes(cap),
197197
)
198198

199-
const canApiKeys = capabilities.includes('api-keys')
199+
const canApiKeys = hasCapability(capabilities, 'api-keys')
200200

201201
const containerRef = React.useRef<HTMLDivElement>(null)
202202

src/db/types.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,55 @@ export interface Role {
186186
createdAt: Date
187187
updatedAt: Date
188188
}
189+
190+
// ============================================================================
191+
// Capability Checking Utilities (isomorphic - works on client and server)
192+
// ============================================================================
193+
194+
/**
195+
* Check if user has a specific capability.
196+
* Admin capability grants access to all other capabilities.
197+
*/
198+
export function hasCapability(
199+
capabilities: Capability[],
200+
requiredCapability: Capability,
201+
): boolean {
202+
return (
203+
capabilities.includes('admin') || capabilities.includes(requiredCapability)
204+
)
205+
}
206+
207+
/**
208+
* Check if user has all specified capabilities.
209+
* Admin capability grants access to all other capabilities.
210+
*/
211+
export function hasAllCapabilities(
212+
capabilities: Capability[],
213+
requiredCapabilities: Capability[],
214+
): boolean {
215+
if (capabilities.includes('admin')) {
216+
return true
217+
}
218+
return requiredCapabilities.every((cap) => capabilities.includes(cap))
219+
}
220+
221+
/**
222+
* Check if user has any of the specified capabilities.
223+
* Admin capability grants access to all other capabilities.
224+
*/
225+
export function hasAnyCapability(
226+
capabilities: Capability[],
227+
requiredCapabilities: Capability[],
228+
): boolean {
229+
if (capabilities.includes('admin')) {
230+
return true
231+
}
232+
return requiredCapabilities.some((cap) => capabilities.includes(cap))
233+
}
234+
235+
/**
236+
* Check if user has admin capability.
237+
*/
238+
export function isAdmin(capabilities: Capability[]): boolean {
239+
return capabilities.includes('admin')
240+
}

src/hooks/useAdPreference.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useCurrentUserQuery } from './useCurrentUser'
22
import { useCapabilities } from './useCapabilities'
3+
import { hasCapability } from '~/db/types'
34
import * as React from 'react'
45

56
const STORAGE_KEY = 'tanstack-ads-preference'
@@ -23,8 +24,7 @@ export function useAdsPreference() {
2324
} else {
2425
const user = userQuery.data
2526
const adsDisabled = user.adsDisabled ?? false
26-
const canDisableAds =
27-
capabilities.includes('admin') || capabilities.includes('disableAds')
27+
const canDisableAds = hasCapability(capabilities, 'disableAds')
2828
adsEnabled = !canDisableAds || !adsDisabled
2929
}
3030

src/hooks/useAdminGuard.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useCurrentUserQuery } from './useCurrentUser'
22
import { useCapabilities } from './useCapabilities'
3-
import type { Capability } from '~/db/types'
3+
import { hasCapability, type Capability } from '~/db/types'
44

55
type AdminGuardLoading = { status: 'loading' }
66
type AdminGuardDenied = { status: 'denied' }
@@ -18,6 +18,8 @@ export type AdminGuardResult =
1818
* Hook to check if the current user has admin access.
1919
* Returns a discriminated union for easy pattern matching.
2020
*
21+
* Note: Users with 'admin' capability are granted access to all capabilities.
22+
*
2123
* @example
2224
* const guard = useAdminGuard()
2325
* if (guard.status === 'loading') return <AdminLoading />
@@ -34,7 +36,7 @@ export function useAdminGuard(
3436
return { status: 'loading' }
3537
}
3638

37-
if (!userQuery.data || !capabilities.includes(requiredCapability)) {
39+
if (!userQuery.data || !hasCapability(capabilities, requiredCapability)) {
3840
return { status: 'denied' }
3941
}
4042

src/routes/_libraries/account.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createFileRoute, Outlet, Link, redirect } from '@tanstack/react-router'
22
import { requireAuth } from '~/utils/auth.server'
33
import { useCapabilities } from '~/hooks/useCapabilities'
4+
import { hasCapability } from '~/db/types'
45

56
export const Route = createFileRoute('/_libraries/account')({
67
component: AccountLayout,
@@ -17,7 +18,7 @@ export const Route = createFileRoute('/_libraries/account')({
1718

1819
function AccountLayout() {
1920
const capabilities = useCapabilities()
20-
const canApiKeys = capabilities.includes('api-keys')
21+
const canApiKeys = hasCapability(capabilities, 'api-keys')
2122

2223
return (
2324
<div className="min-h-screen mx-auto p-4 md:p-8 w-full">

src/routes/_libraries/account/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
44
import { authClient } from '~/utils/auth.client'
55
import { useCurrentUserQuery } from '~/hooks/useCurrentUser'
66
import { useCapabilities } from '~/hooks/useCapabilities'
7+
import { hasCapability } from '~/db/types'
78
import { useToast } from '~/components/ToastProvider'
89
import {
910
updateAdPreference,
@@ -83,8 +84,7 @@ function AccountSettingsPage() {
8384
user && typeof user === 'object' && 'adsDisabled' in user
8485
? (user.adsDisabled ?? false)
8586
: false
86-
const canDisableAds =
87-
capabilities.includes('admin') || capabilities.includes('disableAds')
87+
const canDisableAds = hasCapability(capabilities, 'disableAds')
8888

8989
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
9090
const file = e.target.files?.[0]

0 commit comments

Comments
 (0)