Skip to content

Commit 0088492

Browse files
authored
feat(#1046): Add automatic middleware; fix navigateUnauthenticatedTo (#1054)
* fix: process `navigateUnauthenticatedTo` earlier * feat(#1046): add automatic middleware * chore: run lint fix * docs: improve the middleware documentation
1 parent 23410a4 commit 0088492

File tree

5 files changed

+243
-26
lines changed

5 files changed

+243
-26
lines changed

docs/guide/application-side/protecting-pages.md

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,40 +23,90 @@ export default defineNuxtConfig({
2323
})
2424
```
2525

26-
If you like to further customize the global middleware, you can pass an object of configurations to `globalAppMiddleware`. See the API reference here.
26+
If you'd like to further customize the global middleware, you can pass an object of configurations to `globalAppMiddleware`. See the API reference [here](./configuration#globalappmiddleware).
2727

28-
### Disabling the Global Middleware
29-
30-
If the global middleware is disabled, you can manually add the middleware to individual pages. This is only available if the global middleware is disabled, as you will get an error along the lines of `Error: Unknown route middleware: 'auth'`. This is because the auth middleware is then added globally and not available to use as a local, page-specific middleware.
28+
When you use the global middleware, but want to disable it on some pages, use the [`definePageMeta`](https://nuxt.com/docs/api/utils/define-page-meta) macro to set the authentication metadata for a single page.
3129

3230
```vue
33-
<script lang="ts" setup>
31+
<script setup lang="ts">
3432
definePageMeta({
35-
middleware: 'sidebase-auth'
33+
auth: false // This would disable the middleware for the page
3634
})
3735
</script>
3836
3937
<template>
40-
Only I am protected!
38+
I am not protected anymore!
4139
</template>
4240
```
4341

4442
## Local Middleware
4543

46-
To locally enable or disable the middleware on a single page, you can use the [`definePageMeta`](https://nuxt.com/docs/api/utils/define-page-meta) macro to set the authentication metadata for a single page.
44+
When you are not using the global middleware, you can still protect your pages individually using local middleware. To enable or disable the middleware on a single page, use the `auth` property on `definePageMeta`.
4745

48-
```vue
49-
<script setup lang="ts">
46+
By setting `auth` to `true`, you can automatically enable the middleware on the page:
47+
48+
```vue [Add middleware automatically]
49+
<script lang="ts" setup>
5050
definePageMeta({
51-
auth: false
51+
auth: true // [!code focus]
5252
})
5353
</script>
5454
5555
<template>
56-
I am not protected anymore!
56+
I am now protected again!
5757
</template>
5858
```
5959

60+
:::info
61+
62+
When you use the automatic adding, the middleware will always be added after the other middleware you defined. This means that the following
63+
64+
```ts
65+
definePageMeta({
66+
auth: true,
67+
middleware: 'other-middleware'
68+
})
69+
```
70+
71+
will be interpreted as:
72+
73+
```ts
74+
definePageMeta({
75+
auth: true,
76+
middleware: ['other-middleware', 'sidebase-auth']
77+
})
78+
```
79+
80+
:::
81+
82+
### Manually adding local middleware
83+
84+
Another way of enabling the middleware is by manually specifying it:
85+
86+
```vue [Add middleware manually]
87+
<script lang="ts" setup>
88+
definePageMeta({
89+
middleware: 'sidebase-auth' // [!code focus]
90+
})
91+
</script>
92+
```
93+
94+
This gives you a total control of the order in which the different middlewares are executed:
95+
96+
```vue
97+
<script lang="ts" setup>
98+
definePageMeta({
99+
middleware: ['first-middleware', 'sidebase-auth', 'last-middleware'] // [!code focus]
100+
})
101+
</script>
102+
```
103+
104+
:::warning
105+
106+
Using local middleware is only available if the global middleware was disabled, as otherwise you will get an error along the lines of `Error: Unknown route middleware: 'auth'`. This is because the auth middleware is then added globally and not available to use as a local, page-specific middleware.
107+
108+
:::
109+
60110
### Middleware options
61111

62112
`auth` can be either a boolean or an object of further middleware configurations.
@@ -82,7 +132,7 @@ Whether to allow only unauthenticated users to access this page. Authenticated u
82132

83133
If you want to let everyone see the page, set `auth: false` instead (see [Local Middleware](#local-middleware)).
84134

85-
:::warning
135+
:::danger
86136
This option is required from `0.9.4` onwards to prevent ambiguity ([related issue](https://github.com/sidebase/nuxt-auth/issues/926)). Make sure you set it, otherwise [Guest Mode](#guest-mode) will be **enabled** by default — your guests would be able to see the page, but your authenticated users would be redirected away.
87137
:::
88138

@@ -122,13 +172,9 @@ definePageMeta({
122172

123173
You may create your own application-side middleware in order to implement custom, more advanced authentication logic.
124174

125-
:::warning
126-
Creating a custom middleware is an advanced, experimental option and may result in unexpected or undesired behavior if you are not familiar with advanced Nuxt 3 concepts.
127-
:::
128-
129175
To implement your custom middleware:
130-
- Create an application-side middleware that applies either globally or is named (see the Nuxt docs for more)
131-
- Add logic based on [`useAuth`](/guide/application-side/session-access) to it
176+
- Create an application-side middleware that applies either globally or is named (see the [Nuxt docs](https://nuxt.com/docs/4.x/api/utils/define-page-meta#defining-middleware) for more);
177+
- Add logic based on [`useAuth`](/guide/application-side/session-access) to it.
132178

133179
When adding the logic, you need to watch out when calling other `async` composable functions. This can lead to `context`-problems in Nuxt, see [the explanation for this here](https://github.com/nuxt/framework/issues/5740#issuecomment-1229197529). In order to avoid these problems, you will need to either:
134180

src/build/autoAddMiddleware.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Minimal interface to facilitate unit-testing
2+
export interface NuxtPage {
3+
meta?: Record<string, unknown>
4+
children?: NuxtPage[]
5+
}
6+
7+
/**
8+
* Adds `middleware: ['sidebase-auth']` to pages which use `definePageMeta({ auth: true })` or `definePageMeta({ auth: {} })`.
9+
* @see https://nuxt.com/docs/4.x/guide/directory-structure/app/middleware#setting-middleware-at-build-time
10+
*/
11+
export function autoAddMiddleware(pages: NuxtPage[], middlewareName: string) {
12+
for (const page of pages) {
13+
if (page.meta !== undefined && (page.meta.auth === true || typeof page.meta.auth === 'object')) {
14+
const previousMiddleware: unknown = page.meta.middleware
15+
let normalizedMiddleware: unknown[]
16+
17+
if (previousMiddleware === undefined) {
18+
normalizedMiddleware = []
19+
}
20+
else if (Array.isArray(previousMiddleware)) {
21+
normalizedMiddleware = previousMiddleware
22+
}
23+
else {
24+
normalizedMiddleware = [previousMiddleware]
25+
}
26+
27+
if (!normalizedMiddleware.includes(middlewareName)) {
28+
normalizedMiddleware.push(middlewareName)
29+
page.meta.middleware = normalizedMiddleware
30+
}
31+
}
32+
33+
if (page.children) {
34+
autoAddMiddleware(page.children, middlewareName)
35+
}
36+
}
37+
}

src/module.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
RefreshHandler,
2323
SupportedAuthProviders
2424
} from './runtime/types'
25+
import { autoAddMiddleware } from './build/autoAddMiddleware'
2526

2627
const topLevelDefaults = {
2728
isEnabled: true,
@@ -99,6 +100,7 @@ const defaultsByBackend: {
99100
}
100101

101102
const PACKAGE_NAME = 'sidebase-auth'
103+
const MIDDLEWARE_NAME = PACKAGE_NAME
102104

103105
export default defineNuxtModule<ModuleOptions>({
104106
meta: {
@@ -218,7 +220,6 @@ export default defineNuxtModule<ModuleOptions>({
218220
})
219221

220222
// 5.2. Create refresh handler
221-
// const generatedRefreshHandlerPath = resolve('./runtime/refreshHandler.ts')
222223
const generatedRefreshHandlerPath = addTemplate({
223224
filename: './refreshHandler.ts',
224225
async getContents() {
@@ -241,10 +242,15 @@ export default defineNuxtModule<ModuleOptions>({
241242

242243
// 6. Register middleware for autocomplete in definePageMeta
243244
addRouteMiddleware({
244-
name: 'sidebase-auth',
245+
name: MIDDLEWARE_NAME,
245246
path: resolve('./runtime/middleware/sidebase-auth')
246247
})
247248

249+
// 6.5. Automatically add the middleware when `definePageMeta({ auth: true })` usage is detected
250+
if (!options.globalAppMiddleware || !options.globalAppMiddleware.isEnabled) {
251+
nuxt.hook('pages:extend', pages => autoAddMiddleware(pages, MIDDLEWARE_NAME))
252+
}
253+
248254
// 7. Add plugin for initial load
249255
addPlugin(resolve('./runtime/plugin'))
250256

src/runtime/middleware/sidebase-auth.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ export default defineNuxtRouteMiddleware((to) => {
8888
}
8989
}
9090

91+
// Redirect path was provided
92+
if (options.navigateUnauthenticatedTo) {
93+
return navigateTo(options.navigateUnauthenticatedTo)
94+
}
95+
9196
if (authConfig.provider.type === 'authjs') {
9297
const callbackUrl = determineCallbackUrlForRouteMiddleware(authConfig, to)
9398

@@ -100,11 +105,6 @@ export default defineNuxtRouteMiddleware((to) => {
100105
return signIn(undefined, signInOptions) as Promise<void>
101106
}
102107

103-
// Redirect path was provided
104-
if (options.navigateUnauthenticatedTo) {
105-
return navigateTo(options.navigateUnauthenticatedTo)
106-
}
107-
108108
const loginPage = authConfig.provider.pages.login
109109
if (typeof loginPage !== 'string') {
110110
console.warn(`${ERROR_PREFIX} provider.pages.login is misconfigured`)

tests/autoAddMiddleware.spec.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { autoAddMiddleware } from '../src/build/autoAddMiddleware'
3+
import type { NuxtPage } from '../src/build/autoAddMiddleware'
4+
5+
const MIDDLEWARE_NAME = 'sidebase-auth'
6+
7+
describe('setMiddleware', () => {
8+
it('adds middleware if meta.auth is true', () => {
9+
testMiddleware({ meta: { auth: true } }, [MIDDLEWARE_NAME])
10+
})
11+
12+
it('adds middleware if meta.auth is object', () => {
13+
testMiddleware({ meta: { auth: {} } }, [MIDDLEWARE_NAME])
14+
testMiddleware({ meta: { auth: { navigateUnauthenticatedTo: '/' } } }, [MIDDLEWARE_NAME])
15+
})
16+
17+
it('ignores pages without meta.auth', () => {
18+
testMiddleware({}, undefined)
19+
testMiddleware({ meta: {} }, undefined)
20+
testMiddleware({ meta: { foo: 'bar' } }, undefined)
21+
testMiddleware({ meta: { auth2: 'foo' } }, undefined)
22+
testMiddleware({ meta: { middleware: 'foo' } }, 'foo')
23+
testMiddleware({ meta: { middleware: ['foo'] } }, ['foo'])
24+
const middlewareFunction = () => {}
25+
testMiddleware({ meta: { middleware: middlewareFunction } }, middlewareFunction)
26+
})
27+
28+
it('does not add when meta.auth is false', () => {
29+
testMiddleware({ meta: { auth: false } }, undefined)
30+
})
31+
32+
it('does not add when middleware is already present', () => {
33+
testMiddleware(
34+
{ meta: { auth: true, middleware: MIDDLEWARE_NAME } },
35+
MIDDLEWARE_NAME
36+
)
37+
testMiddleware(
38+
{ meta: { auth: true, middleware: [MIDDLEWARE_NAME] } },
39+
[MIDDLEWARE_NAME]
40+
)
41+
testMiddleware(
42+
{ meta: { auth: true, middleware: ['foo', MIDDLEWARE_NAME, 'bar'] } },
43+
['foo', MIDDLEWARE_NAME, 'bar']
44+
)
45+
testMiddleware(
46+
{ meta: { middleware: MIDDLEWARE_NAME } },
47+
MIDDLEWARE_NAME
48+
)
49+
testMiddleware(
50+
{ meta: { middleware: [MIDDLEWARE_NAME] } },
51+
[MIDDLEWARE_NAME]
52+
)
53+
testMiddleware(
54+
{ meta: { middleware: ['foo', MIDDLEWARE_NAME, 'bar'] } },
55+
['foo', MIDDLEWARE_NAME, 'bar']
56+
)
57+
})
58+
59+
it('adds to an existing array', () => {
60+
testMiddleware(
61+
{ meta: { auth: true, middleware: [] } },
62+
[MIDDLEWARE_NAME]
63+
)
64+
testMiddleware(
65+
{ meta: { auth: true, middleware: ['foo'] } },
66+
['foo', MIDDLEWARE_NAME]
67+
)
68+
})
69+
70+
it('wraps string middleware into array', () => {
71+
testMiddleware(
72+
{ meta: { auth: true, middleware: 'foo' } },
73+
['foo', MIDDLEWARE_NAME]
74+
)
75+
})
76+
77+
it('overrides undefined', () => {
78+
testMiddleware(
79+
{ meta: { auth: true, middleware: undefined } },
80+
[MIDDLEWARE_NAME]
81+
)
82+
})
83+
84+
it('wraps other middleware options into array', () => {
85+
const functionMiddleware = () => {}
86+
testMiddleware(
87+
{ meta: { auth: true, middleware: functionMiddleware } },
88+
[functionMiddleware, MIDDLEWARE_NAME]
89+
)
90+
})
91+
92+
it('handles multiple pages', () => {
93+
const pages: NuxtPage[] = [
94+
{ meta: { auth: true } },
95+
{ meta: { auth: true, middleware: 'foo' } }
96+
]
97+
98+
autoAddMiddleware(pages, MIDDLEWARE_NAME)
99+
100+
expect(pages[0].meta?.middleware).toEqual([MIDDLEWARE_NAME])
101+
expect(pages[1].meta?.middleware).toEqual(['foo', MIDDLEWARE_NAME])
102+
})
103+
104+
it('handles nested children', () => {
105+
const pages: NuxtPage[] = [
106+
{
107+
meta: {},
108+
children: [
109+
{ meta: { auth: true } }
110+
]
111+
}
112+
]
113+
114+
autoAddMiddleware(pages, MIDDLEWARE_NAME)
115+
116+
expect(pages[0].meta?.middleware).toBeUndefined()
117+
expect(pages[0].children?.[0].meta?.middleware).toEqual([MIDDLEWARE_NAME])
118+
})
119+
})
120+
121+
/**
122+
* Helper: test a single-page scenario
123+
*/
124+
function testMiddleware(page: NuxtPage, expected: unknown) {
125+
const pages: NuxtPage[] = [page]
126+
autoAddMiddleware(pages, MIDDLEWARE_NAME)
127+
expect(pages[0].meta?.middleware).toEqual(expected)
128+
}

0 commit comments

Comments
 (0)