Skip to content

Commit c7c5b09

Browse files
committed
refactor route protection with typed UserMatch and auto-inferred types
1 parent e9d1ba2 commit c7c5b09

File tree

18 files changed

+468
-137
lines changed

18 files changed

+468
-137
lines changed

docs/app/components/app/AppHeader.vue

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
<script setup lang="ts">
2-
const route = useRoute()
32
const appConfig = useAppConfig()
43
const site = useSiteConfig()
54
65
const navLinks = [
76
{ name: 'docs', path: '/getting-started/quickstart' },
87
{ name: 'better-auth', path: 'https://www.better-auth.com', external: true },
98
]
10-
11-
const isLanding = computed(() => route.path === '/')
129
</script>
1310

1411
<template>

docs/app/components/content/landing/LandingFeatures.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ const items = features.items as { title: string, description: string, icon: stri
2525
>
2626
<div class="flex items-center gap-2 my-1">
2727
<UIcon :name="feature.icon" class="size-4" />
28-
<p class="text-stone-600 dark:text-stone-400">{{ feature.title }}</p>
28+
<p class="text-stone-600 dark:text-stone-400">
29+
{{ feature.title }}
30+
</p>
2931
</div>
3032
<div class="mt-2">
3133
<p class="mt-2 text-sm text-left text-muted">
@@ -44,7 +46,9 @@ const items = features.items as { title: string, description: string, icon: stri
4446
<div class="flex flex-col items-center justify-center w-full h-full gap-3">
4547
<div class="flex items-center gap-2">
4648
<UIcon name="i-lucide-globe" class="size-4" />
47-
<p class="text-stone-600 dark:text-stone-400">Nuxt + Better Auth</p>
49+
<p class="text-stone-600 dark:text-stone-400">
50+
Nuxt + Better Auth
51+
</p>
4852
</div>
4953
<p class="max-w-md mx-auto mt-4 text-4xl font-normal tracking-tighter text-center md:text-4xl">
5054
<strong>Set up authentication in minutes, not hours!</strong>

docs/app/components/content/landing/LandingHero.vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ const tabs = hero.tabs as { name: string, code: string, lang: string }[]
77
88
// Map file extensions to languages
99
function getLang(filename: string) {
10-
if (filename.endsWith('.ts')) return 'ts'
11-
if (filename.endsWith('.vue')) return 'vue'
12-
if (filename.endsWith('.js')) return 'js'
10+
if (filename.endsWith('.ts'))
11+
return 'ts'
12+
if (filename.endsWith('.vue'))
13+
return 'vue'
14+
if (filename.endsWith('.js'))
15+
return 'js'
1316
return 'ts'
1417
}
1518

docs/content/1.getting-started/4.type-augmentation.md

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
11
---
22
title: Type Augmentation
3-
description: Extend Better Auth user and session types in your app.
3+
description: Types are automatically inferred from your auth config.
44
---
55

6-
Nuxt Better Auth exposes augmentable types under `#nuxt-better-auth`.
6+
## Automatic Type Inference
77

8-
Create `shared/types/auth.d.ts`:
8+
Types for `AuthUser` and `AuthSession` are automatically inferred from your `server/auth.config.ts`. No manual type augmentation needed for plugin fields!
99

1010
```ts
11+
// server/auth.config.ts
12+
import { admin } from 'better-auth/plugins'
13+
14+
export default defineServerAuth(() => ({
15+
plugins: [admin()],
16+
}))
17+
18+
// Now user.role is typed automatically in your app!
19+
```
20+
21+
## HMR Support
22+
23+
Changes to `auth.config.ts` automatically update types - no restart needed.
24+
25+
## Custom Fields (Optional)
26+
27+
If you need custom fields not from plugins, you can still use module augmentation:
28+
29+
```ts
30+
// shared/types/auth.d.ts
1131
import '#nuxt-better-auth'
1232

1333
declare module '#nuxt-better-auth' {
1434
interface AuthUser {
15-
role?: string | null
16-
banned?: boolean | null
35+
customField?: string
1736
}
1837
}
1938
```
20-
21-
After this, `useUserSession()` and server utilities will reflect your custom fields.
22-
Lines changed: 114 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,138 @@
11
---
22
title: Route Protection
3-
description: Protect pages with route rules or meta.
3+
description: Layered approach to protect routes on client and server.
44
---
55

6-
There are two protection layers:
6+
## Layer 1: Route Rules
77

8-
## Client pages
8+
Declarative protection in `nuxt.config.ts`. Covers 90% of use cases:
99

10-
Global route middleware reads `meta.auth` / `meta.role`:
10+
```ts
11+
export default defineNuxtConfig({
12+
routeRules: {
13+
'/app/**': { auth: 'user' },
14+
'/login': { auth: 'guest' },
15+
'/admin/**': { auth: { user: { role: 'admin' } } },
16+
'/staff/**': { auth: { user: { role: ['admin', 'moderator'] } } },
17+
},
18+
})
19+
```
20+
21+
## Layer 2: Page Meta
22+
23+
Per-page overrides using `definePageMeta`. Same syntax as `routeRules`:
1124

1225
```ts
1326
definePageMeta({
14-
auth: 'user', // or 'guest'
15-
// role: 'admin', // optional
27+
auth: 'user',
28+
})
29+
30+
// Or with user matching
31+
definePageMeta({
32+
auth: { user: { role: 'admin' } },
33+
})
34+
35+
// Custom redirect
36+
definePageMeta({
37+
auth: { only: 'user', redirectTo: '/subscribe' },
1638
})
1739
```
1840

19-
If a user is not logged in and `auth` resolves to `'user'`, they are redirected to `/login`.
41+
## Layer 3: Custom Middleware
2042

21-
## Route rules sync
43+
For complex client-side logic, create standard Nuxt route middleware:
2244

23-
The module copies `routeRules.auth` and `routeRules.role` into page meta at build time:
45+
```ts
46+
// middleware/premium.ts
47+
export default defineNuxtRouteMiddleware(() => {
48+
const { user } = useUserSession()
49+
50+
if (!user.value?.subscription?.active) {
51+
return navigateTo('/pricing')
52+
}
53+
})
54+
```
2455

2556
```ts
57+
// pages/premium.vue
58+
definePageMeta({
59+
middleware: 'premium',
60+
})
61+
```
62+
63+
## Layer 4: Server API Protection
64+
65+
Use `requireUserSession()` for server-side checks:
66+
67+
```ts
68+
// server/api/admin/stats.get.ts
69+
export default defineEventHandler(async (event) => {
70+
const { user } = await requireUserSession(event)
71+
72+
// User is guaranteed to exist
73+
return { stats: await getStats(user.id) }
74+
})
75+
```
76+
77+
With user matching:
78+
79+
```ts
80+
// server/api/admin/users.get.ts
81+
export default defineEventHandler(async (event) => {
82+
await requireUserSession(event, { user: { role: 'admin' } })
83+
84+
return { users: await getAllUsers() }
85+
})
86+
```
87+
88+
## Module Configuration
89+
90+
```ts
91+
// nuxt.config.ts
2692
export default defineNuxtConfig({
27-
routeRules: {
28-
'/app/**': { auth: 'user' },
29-
'/admin/**': { auth: { role: 'admin' } },
30-
'/login': { auth: 'guest' },
93+
auth: {
94+
redirects: {
95+
login: '/login', // Where to send unauthenticated users
96+
guest: '/', // Where to send authenticated users from guest-only pages
97+
},
3198
},
3299
})
33100
```
34101

35-
### `auth` values
102+
## AuthMeta Types
103+
104+
| Value | Description |
105+
|-------|-------------|
106+
| `false` | Public, no checks |
107+
| `'guest'` | Unauthenticated only |
108+
| `'user'` | Authenticated only |
109+
| `{ only?, redirectTo?, user? }` | Object form with options |
110+
111+
Object form options:
112+
- `only: 'guest' | 'user'` - access restriction
113+
- `redirectTo: string` - custom redirect path
114+
- `user: UserMatch` - match against user properties
115+
116+
## UserMatch Syntax
36117

37-
- `false` or `undefined`: public
38-
- `'guest'`: only unauthenticated users
39-
- `'user'`: any authenticated user
40-
- `{ only?: 'guest' | 'user', role?: string | string[], redirectTo?: string }`
118+
Match user properties for fine-grained access control:
119+
120+
```ts
121+
// Exact match - user.role must equal 'admin'
122+
{ user: { role: 'admin' } }
123+
124+
// OR logic - user.role must be 'admin' OR 'moderator'
125+
{ user: { role: ['admin', 'moderator'] } }
126+
127+
// AND logic - multiple fields must all match
128+
{ user: { role: 'admin', verified: true } }
129+
130+
// Combined - role is admin OR moderator, AND verified is true
131+
{ user: { role: ['admin', 'moderator'], verified: true } }
132+
```
41133

42-
## See also
134+
## See Also
43135

44-
- How the module works: `/core-concepts/how-it-works`
45-
- Security & caveats: `/core-concepts/security-caveats`
136+
- [How It Works](/core-concepts/how-it-works)
137+
- [Security Caveats](/core-concepts/security-caveats)
138+
- [API Protection Guide](/guides/api-protection)
Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,57 @@
11
---
22
title: Role‑Based Access
3-
description: Enforce roles on pages and server handlers.
3+
description: Protect routes using generic field matching on AuthUser.
44
---
55

6-
Roles are free‑form strings on your `AuthUser`.
7-
Augment the type if you store `role` or `banned` on users (see `/getting-started/type-augmentation`).
6+
Route protection uses generic field matching via the `user` key. Works with ANY field on your `AuthUser` - not just roles.
87

9-
## Pages
8+
## With Admin Plugin
109

1110
```ts
12-
definePageMeta({
13-
auth: { role: 'admin' },
14-
})
11+
// server/auth.config.ts
12+
import { admin } from 'better-auth/plugins'
13+
export default defineServerAuth(() => ({ plugins: [admin()] }))
14+
15+
// nuxt.config.ts - role is now typed!
16+
routeRules: {
17+
'/admin/**': { auth: { user: { role: 'admin' } } },
18+
'/staff/**': { auth: { user: { role: ['admin', 'moderator'] } } },
19+
}
20+
```
21+
22+
## With Organization Plugin
23+
24+
```ts
25+
// Works the same way with any plugin fields
26+
routeRules: {
27+
'/team/**': { auth: { user: { teamRole: 'owner' } } },
28+
}
1529
```
1630

17-
## Server
31+
## With Custom Fields
1832

1933
```ts
20-
export default defineEventHandler(async (event) => {
21-
const { user } = await requireUserSession(event, { role: 'admin' })
22-
return { ok: true, user }
23-
})
34+
// Any field on AuthUser works
35+
routeRules: {
36+
'/premium/**': { auth: { user: { isPremium: true } } },
37+
'/verified/**': { auth: { user: { emailVerified: true } } },
38+
}
39+
```
40+
41+
## Matching Logic
42+
43+
- **Single value**: exact match required
44+
- **Array**: OR logic (user field must be one of the values)
45+
- **Multiple fields**: AND logic (all must match)
46+
47+
```ts
48+
// Must be admin AND verified
49+
{ auth: { user: { role: 'admin', emailVerified: true } } }
50+
51+
// Must be admin OR moderator
52+
{ auth: { user: { role: ['admin', 'moderator'] } } }
2453
```
2554

26-
`requireUserSession` also checks `user.banned` and throws `403` if true.
55+
## Complex Logic
2756

57+
For complex authorization, use custom middleware or `requireUserSession` with a `rule` callback.

0 commit comments

Comments
 (0)