Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 65 additions & 19 deletions docs/guide/application-side/protecting-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,40 +23,90 @@ export default defineNuxtConfig({
})
```

If you like to further customize the global middleware, you can pass an object of configurations to `globalAppMiddleware`. See the API reference here.
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).

### Disabling the Global Middleware

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.
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.

```vue
<script lang="ts" setup>
<script setup lang="ts">
definePageMeta({
middleware: 'sidebase-auth'
auth: false // This would disable the middleware for the page
})
</script>

<template>
Only I am protected!
I am not protected anymore!
</template>
```

## Local Middleware

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.
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`.

```vue
<script setup lang="ts">
By setting `auth` to `true`, you can automatically enable the middleware on the page:

```vue [Add middleware automatically]
<script lang="ts" setup>
definePageMeta({
auth: false
auth: true // [!code focus]
})
</script>

<template>
I am not protected anymore!
I am now protected again!
</template>
```

:::info

When you use the automatic adding, the middleware will always be added after the other middleware you defined. This means that the following

```ts
definePageMeta({
auth: true,
middleware: 'other-middleware'
})
```

will be interpreted as:

```ts
definePageMeta({
auth: true,
middleware: ['other-middleware', 'sidebase-auth']
})
```

:::

### Manually adding local middleware

Another way of enabling the middleware is by manually specifying it:

```vue [Add middleware manually]
<script lang="ts" setup>
definePageMeta({
middleware: 'sidebase-auth' // [!code focus]
})
</script>
```

This gives you a total control of the order in which the different middlewares are executed:

```vue
<script lang="ts" setup>
definePageMeta({
middleware: ['first-middleware', 'sidebase-auth', 'last-middleware'] // [!code focus]
})
</script>
```

:::warning

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.

:::

### Middleware options

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

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

:::warning
:::danger
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.
:::

Expand Down Expand Up @@ -122,13 +172,9 @@ definePageMeta({

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

:::warning
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.
:::

To implement your custom middleware:
- Create an application-side middleware that applies either globally or is named (see the Nuxt docs for more)
- Add logic based on [`useAuth`](/guide/application-side/session-access) to it
- 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);
- Add logic based on [`useAuth`](/guide/application-side/session-access) to it.

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:

Expand Down
37 changes: 37 additions & 0 deletions src/build/autoAddMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Minimal interface to facilitate unit-testing
export interface NuxtPage {
meta?: Record<string, unknown>
children?: NuxtPage[]
}

/**
* Adds `middleware: ['sidebase-auth']` to pages which use `definePageMeta({ auth: true })` or `definePageMeta({ auth: {} })`.
* @see https://nuxt.com/docs/4.x/guide/directory-structure/app/middleware#setting-middleware-at-build-time
*/
export function autoAddMiddleware(pages: NuxtPage[], middlewareName: string) {
for (const page of pages) {
if (page.meta !== undefined && (page.meta.auth === true || typeof page.meta.auth === 'object')) {
const previousMiddleware: unknown = page.meta.middleware
let normalizedMiddleware: unknown[]

if (previousMiddleware === undefined) {
normalizedMiddleware = []
}
else if (Array.isArray(previousMiddleware)) {
normalizedMiddleware = previousMiddleware
}
else {
normalizedMiddleware = [previousMiddleware]
}

if (!normalizedMiddleware.includes(middlewareName)) {
normalizedMiddleware.push(middlewareName)
page.meta.middleware = normalizedMiddleware
}
}

if (page.children) {
autoAddMiddleware(page.children, middlewareName)
}
}
}
10 changes: 8 additions & 2 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
RefreshHandler,
SupportedAuthProviders
} from './runtime/types'
import { autoAddMiddleware } from './build/autoAddMiddleware'

const topLevelDefaults = {
isEnabled: true,
Expand Down Expand Up @@ -99,6 +100,7 @@ const defaultsByBackend: {
}

const PACKAGE_NAME = 'sidebase-auth'
const MIDDLEWARE_NAME = PACKAGE_NAME

export default defineNuxtModule<ModuleOptions>({
meta: {
Expand Down Expand Up @@ -218,7 +220,6 @@ export default defineNuxtModule<ModuleOptions>({
})

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

// 6. Register middleware for autocomplete in definePageMeta
addRouteMiddleware({
name: 'sidebase-auth',
name: MIDDLEWARE_NAME,
path: resolve('./runtime/middleware/sidebase-auth')
})

// 6.5. Automatically add the middleware when `definePageMeta({ auth: true })` usage is detected
if (!options.globalAppMiddleware || !options.globalAppMiddleware.isEnabled) {
nuxt.hook('pages:extend', pages => autoAddMiddleware(pages, MIDDLEWARE_NAME))
}

// 7. Add plugin for initial load
addPlugin(resolve('./runtime/plugin'))

Expand Down
10 changes: 5 additions & 5 deletions src/runtime/middleware/sidebase-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export default defineNuxtRouteMiddleware((to) => {
}
}

// Redirect path was provided
if (options.navigateUnauthenticatedTo) {
return navigateTo(options.navigateUnauthenticatedTo)
}

if (authConfig.provider.type === 'authjs') {
const callbackUrl = determineCallbackUrlForRouteMiddleware(authConfig, to)

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

// Redirect path was provided
if (options.navigateUnauthenticatedTo) {
return navigateTo(options.navigateUnauthenticatedTo)
}

const loginPage = authConfig.provider.pages.login
if (typeof loginPage !== 'string') {
console.warn(`${ERROR_PREFIX} provider.pages.login is misconfigured`)
Expand Down
128 changes: 128 additions & 0 deletions tests/autoAddMiddleware.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { describe, expect, it } from 'vitest'
import { autoAddMiddleware } from '../src/build/autoAddMiddleware'
import type { NuxtPage } from '../src/build/autoAddMiddleware'

const MIDDLEWARE_NAME = 'sidebase-auth'

describe('setMiddleware', () => {
it('adds middleware if meta.auth is true', () => {
testMiddleware({ meta: { auth: true } }, [MIDDLEWARE_NAME])
})

it('adds middleware if meta.auth is object', () => {
testMiddleware({ meta: { auth: {} } }, [MIDDLEWARE_NAME])
testMiddleware({ meta: { auth: { navigateUnauthenticatedTo: '/' } } }, [MIDDLEWARE_NAME])
})

it('ignores pages without meta.auth', () => {
testMiddleware({}, undefined)
testMiddleware({ meta: {} }, undefined)
testMiddleware({ meta: { foo: 'bar' } }, undefined)
testMiddleware({ meta: { auth2: 'foo' } }, undefined)
testMiddleware({ meta: { middleware: 'foo' } }, 'foo')
testMiddleware({ meta: { middleware: ['foo'] } }, ['foo'])
const middlewareFunction = () => {}
testMiddleware({ meta: { middleware: middlewareFunction } }, middlewareFunction)
})

it('does not add when meta.auth is false', () => {
testMiddleware({ meta: { auth: false } }, undefined)
})

it('does not add when middleware is already present', () => {
testMiddleware(
{ meta: { auth: true, middleware: MIDDLEWARE_NAME } },
MIDDLEWARE_NAME
)
testMiddleware(
{ meta: { auth: true, middleware: [MIDDLEWARE_NAME] } },
[MIDDLEWARE_NAME]
)
testMiddleware(
{ meta: { auth: true, middleware: ['foo', MIDDLEWARE_NAME, 'bar'] } },
['foo', MIDDLEWARE_NAME, 'bar']
)
testMiddleware(
{ meta: { middleware: MIDDLEWARE_NAME } },
MIDDLEWARE_NAME
)
testMiddleware(
{ meta: { middleware: [MIDDLEWARE_NAME] } },
[MIDDLEWARE_NAME]
)
testMiddleware(
{ meta: { middleware: ['foo', MIDDLEWARE_NAME, 'bar'] } },
['foo', MIDDLEWARE_NAME, 'bar']
)
})

it('adds to an existing array', () => {
testMiddleware(
{ meta: { auth: true, middleware: [] } },
[MIDDLEWARE_NAME]
)
testMiddleware(
{ meta: { auth: true, middleware: ['foo'] } },
['foo', MIDDLEWARE_NAME]
)
})

it('wraps string middleware into array', () => {
testMiddleware(
{ meta: { auth: true, middleware: 'foo' } },
['foo', MIDDLEWARE_NAME]
)
})

it('overrides undefined', () => {
testMiddleware(
{ meta: { auth: true, middleware: undefined } },
[MIDDLEWARE_NAME]
)
})

it('wraps other middleware options into array', () => {
const functionMiddleware = () => {}
testMiddleware(
{ meta: { auth: true, middleware: functionMiddleware } },
[functionMiddleware, MIDDLEWARE_NAME]
)
})

it('handles multiple pages', () => {
const pages: NuxtPage[] = [
{ meta: { auth: true } },
{ meta: { auth: true, middleware: 'foo' } }
]

autoAddMiddleware(pages, MIDDLEWARE_NAME)

expect(pages[0].meta?.middleware).toEqual([MIDDLEWARE_NAME])
expect(pages[1].meta?.middleware).toEqual(['foo', MIDDLEWARE_NAME])
})

it('handles nested children', () => {
const pages: NuxtPage[] = [
{
meta: {},
children: [
{ meta: { auth: true } }
]
}
]

autoAddMiddleware(pages, MIDDLEWARE_NAME)

expect(pages[0].meta?.middleware).toBeUndefined()
expect(pages[0].children?.[0].meta?.middleware).toEqual([MIDDLEWARE_NAME])
})
})

/**
* Helper: test a single-page scenario
*/
function testMiddleware(page: NuxtPage, expected: unknown) {
const pages: NuxtPage[] = [page]
autoAddMiddleware(pages, MIDDLEWARE_NAME)
expect(pages[0].meta?.middleware).toEqual(expected)
}
Loading