diff --git a/README.md b/README.md
index 2ec734e..93bf1a0 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@ export default defineNuxtConfig({
// },
// registerComponents: true,
// augmentContext: true,
+ // globalMiddleware: false,
},
})
```
@@ -63,17 +64,27 @@ Check out the [Hanko documentation](https://docs.hanko.io/guides/vue) to learn m
### Middleware
-By default two new route middleware are available in your Nuxt app: `hanko-logged-in` and `hanko-logged-out`.
+By default three new route middlewares are available in your Nuxt app: `hanko-logged-in`, `hanko-logged-out` and `hanko-allow-all`.
- `hanko-logged-in` will prevent access to a page unless you are logged in. (It will redirect you to `redirects.login` instead, and then redirect back to this page once you login. You can disable this behaviour by setting `redirects.followRedirect` to `false`.)
- `hanko-logged-out` will prevent access to a page unless you are logged out. (It will redirect you to `redirects.success` when you log in, and otherwise to `redirects.home`.)
+- `hanko-allow-all` will allow all users access to page, even if the `globalMiddleware` option is set to `true`. (If the `globalMiddleware` option is not set to `true`, this middleware has no effect.)
You can also create your own middleware for full control.
+### Global Middleware
+
+If the `globalMiddleware` configuration is set to `true`, the middleware is automatically applied to all of your pages.
+You can still override this behavior on each page, by applying the `hanko-logged-out` or the `hanko-allow-all` middleware.
+
+**Note**: The `globalMiddleware` option will not apply any authentication checks to your API-paths.
+
### Auto-imports
`useHanko` is exposed in the Vue part of your app to allow you direct access to the Hanko API. You can access the current user and much more. **Note**: It will return `null` on the server.
+The `hankoLoggedIn` and `hankoLoggedOut` middleware are exposed to enable you to extend their functionality, such as creating a custom global middleware.
+
### Server utilities
By default you can access a verified JWT context on `event.context.hanko`. (It will be undefined if the user is not logged in.) If you want to handle this yourself you can set `augmentContext: false`.
diff --git a/playground/pages/about.vue b/playground/pages/about.vue
index c288fdb..c15f44a 100644
--- a/playground/pages/about.vue
+++ b/playground/pages/about.vue
@@ -1,3 +1,9 @@
+
+
About
diff --git a/src/module.ts b/src/module.ts
index 5342b86..02245d8 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -16,6 +16,7 @@ export interface ModuleOptions {
apiURL?: string
registerComponents?: boolean
augmentContext?: boolean
+ globalMiddleware?: boolean
cookieName?: string
redirects?: {
login?: string
@@ -34,6 +35,7 @@ export default defineNuxtModule({
apiURL: '',
registerComponents: true,
augmentContext: true,
+ globalMiddleware: false,
cookieName: 'hanko',
redirects: {
login: '/login',
@@ -86,7 +88,15 @@ export default defineNuxtModule({
})
}
- for (const name of ['logged-in', 'logged-out']) {
+ if (options.globalMiddleware) {
+ addRouteMiddleware({
+ name: 'hanko-global-logged-in',
+ path: resolver.resolve('./runtime/middleware/global-logged-in'),
+ global: true,
+ })
+ }
+
+ for (const name of ['allow-all', 'logged-in', 'logged-out']) {
addRouteMiddleware({
name: `hanko-${name}`,
path: resolver.resolve(`./runtime/middleware/${name}`),
@@ -98,6 +108,14 @@ export default defineNuxtModule({
from: resolver.resolve('./runtime/composables/index'),
imports: ['useHanko'],
})
+ addImportsSources({
+ from: resolver.resolve('./runtime/middleware/logged-in.ts'),
+ imports: ['hankoLoggedIn'],
+ })
+ addImportsSources({
+ from: resolver.resolve('./runtime/middleware/logged-out.ts'),
+ imports: ['hankoLoggedOut'],
+ })
const hankoElementsTemplate = addTemplate({
filename: 'hanko-elements.mjs',
diff --git a/src/runtime/middleware/allow-all.ts b/src/runtime/middleware/allow-all.ts
new file mode 100644
index 0000000..ceb8553
--- /dev/null
+++ b/src/runtime/middleware/allow-all.ts
@@ -0,0 +1,3 @@
+import { defineNuxtRouteMiddleware } from '#imports'
+
+export default defineNuxtRouteMiddleware(() => true)
diff --git a/src/runtime/middleware/global-logged-in.ts b/src/runtime/middleware/global-logged-in.ts
new file mode 100644
index 0000000..146400f
--- /dev/null
+++ b/src/runtime/middleware/global-logged-in.ts
@@ -0,0 +1,41 @@
+import { defineNuxtRouteMiddleware, hankoLoggedIn } from '#imports'
+
+export default defineNuxtRouteMiddleware(async (to, from) => {
+ // If the requested location doesn't exist: let the router handle it
+ if (to.matched.length === 0) return true
+
+ // Don't trigger on client-side navigation on the same-page
+ // (changes in to.query or to.hash)
+ if (!import.meta.server && to.path === from.path) return true
+
+ // If a hanko middleware is explicitly set, that middleware handles
+ // navigation and the default hankoLoggedIn is skipped
+ if (Array.isArray(to.meta.middleware)) {
+ if (to.meta.middleware.some(isHankoMiddleware)) {
+ return true
+ }
+ }
+ else if (isHankoMiddleware(to.meta.middleware)) {
+ return true
+ }
+
+ // If no hanko middleware is set, default to hankoLoggedIn
+ return await hankoLoggedIn(to, from)
+})
+
+/**
+ * Checks if the given middleware is a valid Hanko middleware.
+ *
+ * @param middleware - The middleware to check.
+ * @description If middleware is undefined or a NavigationGuard (function), it is not a Hanko middleware.
+ * A valid Hanko middleware is a MiddlewareKey (string) with one of the following values:
+ * - `hanko-logged-in`
+ * - `hanko-logged-out`
+ * - `hanko-allow-all`
+ */
+const isHankoMiddleware = (middleware: unknown) => {
+ return (
+ typeof middleware === 'string'
+ && ['hanko-allow-all', 'hanko-logged-in', 'hanko-logged-out'].includes(middleware)
+ )
+}
diff --git a/src/runtime/middleware/logged-in.ts b/src/runtime/middleware/logged-in.ts
index 2f40ce0..2eebe63 100644
--- a/src/runtime/middleware/logged-in.ts
+++ b/src/runtime/middleware/logged-in.ts
@@ -1,4 +1,5 @@
import { withQuery } from 'ufo'
+import type { RouteMiddleware } from '#app'
import {
defineNuxtRouteMiddleware,
navigateTo,
@@ -8,7 +9,7 @@ import {
useRequestEvent,
} from '#imports'
-export default defineNuxtRouteMiddleware(async (to) => {
+export const hankoLoggedIn: RouteMiddleware = async (to) => {
const redirects = useAppConfig().hanko.redirects
if (import.meta.server) {
@@ -33,4 +34,6 @@ export default defineNuxtRouteMiddleware(async (to) => {
removeHankoHook()
removeRouterHook()
})
-})
+}
+
+export default defineNuxtRouteMiddleware(hankoLoggedIn)
diff --git a/src/runtime/middleware/logged-out.ts b/src/runtime/middleware/logged-out.ts
index 7ee56b6..9bead14 100644
--- a/src/runtime/middleware/logged-out.ts
+++ b/src/runtime/middleware/logged-out.ts
@@ -1,3 +1,4 @@
+import type { RouteMiddleware } from '#app'
import {
defineNuxtRouteMiddleware,
navigateTo,
@@ -7,7 +8,7 @@ import {
useRequestEvent,
} from '#imports'
-export default defineNuxtRouteMiddleware(async (to) => {
+export const hankoLoggedOut: RouteMiddleware = async (to) => {
const redirects = useAppConfig().hanko.redirects
if (import.meta.server) {
@@ -36,4 +37,6 @@ export default defineNuxtRouteMiddleware(async (to) => {
removeHankoHook()
removeRouterHook()
})
-})
+}
+
+export default defineNuxtRouteMiddleware(hankoLoggedOut)
diff --git a/test/globalMiddleware.test.ts b/test/globalMiddleware.test.ts
new file mode 100644
index 0000000..aed4e70
--- /dev/null
+++ b/test/globalMiddleware.test.ts
@@ -0,0 +1,63 @@
+import { fileURLToPath } from 'node:url'
+import { describe, it, expect } from 'vitest'
+import { setup, fetch } from '@nuxt/test-utils'
+
+await setup({
+ rootDir: fileURLToPath(new URL('../playground', import.meta.url)),
+ nuxtConfig: { hanko: { globalMiddleware: true } },
+})
+
+const fetchOptions: RequestInit = { redirect: 'manual' }
+
+describe('logged-out user', async () => {
+ it('allows pages with `hanko-logged-out` middleware', async () => {
+ const res = await fetch('/login', fetchOptions)
+ expect(res.status).toBe(200)
+ })
+
+ it('`hanko-logged-in` middleware redirects to the login page', async () => {
+ const res = await fetch('/protected', fetchOptions)
+ expect(res.headers.get('location')).toBe('/login?redirect=/protected')
+ })
+
+ it('global middleware redirects on page without explicit middleware', async () => {
+ const res = await fetch('/', fetchOptions)
+ expect(res.headers.get('location')).toBe('/login?redirect=/')
+ })
+
+ it('allows `hanko-allow-all` with global middleware', async () => {
+ const res = await fetch('/about', fetchOptions)
+ expect(res.status).toBe(200)
+ })
+})
+
+describe('logged-in user', async () => {
+ const authenticatedFetchOptions: RequestInit = {
+ ...fetchOptions,
+ headers: {
+ cookie:
+ // This will not work. It was an actually valid cookie, signed for localhost:3000, but it has a lifetime of 1 hour.
+ 'hanko=eyJhbGciOiJSUzI1NiIsImtpZCI6IjVjYzU1MDFmLWYxNWQtNDY0Ni1iMmRiLTlkNzcwODgwYWM0NCIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibG9jYWxob3N0Il0sImVtYWlsIjp7ImFkZHJlc3MiOiJtYWlsQGZlbGl4ZG9sZGVyZXIuY29tIiwiaXNfcHJpbWFyeSI6dHJ1ZSwiaXNfdmVyaWZpZWQiOnRydWV9LCJleHAiOjE3MjM4NTQ2NDQsImlhdCI6MTcyMzgxMTQ0NCwic3ViIjoiN2JiNzE1MDktMWU4YS00MGY5LTlmY2EtM2ViZjg1NjdjNGJkIn0.wSzfcdCcqs-oQB2Ifq-bKqfJTxwIS9q1xw_2E6lVeRd3dyIcJPtaSHAY-mS798GjKuC9DpkaHQyWVV28xcriY7Csvu09vrFfcqcX1cj_y9eKQPp9J_q1Dt8-Q5hOuXasMxQZg9s9R0OgLXVXqQ_G8M5BBu68ahANR9pK1cxyJ0QN5u04FpK5xmgNsdgKvEqaQqgbdvU5h-7F-GBl07of8c4VTd2NmuNl9jINdeTSYpkbEFEDljjG4ll2tw3CSfCSR_aonz3WR2R2bjbMuIVPW6HnDKcAdJ2eXdF7L3kfqg-NdA-IrPef9GlmlFDNbrTdrH8WWhZ17x-euRsJd08VAD0TiJWPoPyV_S0T9W-J6Dsle0zn6frbXkzcQnDQVEZv7w6nCWZaAhfqEbJUni9aE8BDskOZRC2aRDyntfGTymfUo0KheJ5NgzaWEME4VAT0zhWymJwOaY1LqdSeYowu1iet_yYnq6fdWPKRehxvZGIFsfKRBcO17INGwDQFDn0n6c1zsIqUYTNh6vp0H4q3mdwQPsnC2gtVA8phYiRr0uznumfLIjEoB53xvU2l0r_IVZukDY89kqxkdgwe1o5kl5Z43cgAG1Ohv1EVeM0z7aYYMqrrDfC7lTxTSVG07IjtP75NMZVKsmUzhKLtM1PaIuEYUi4RPlRxBsizJag_MIs',
+ },
+ }
+
+ it('allows pages with `hanko-logged-in` middleware', async () => {
+ const res = await fetch('/protected', authenticatedFetchOptions)
+ expect(res.status).toBe(200)
+ })
+
+ it('`hanko-logged-out` redirects logged-in user to the home page', async () => {
+ const res = await fetch('/login', authenticatedFetchOptions)
+ expect(res.headers.get('location')).toBe('/')
+ })
+
+ it('allows page without explicit middleware', async () => {
+ const res = await fetch('/', authenticatedFetchOptions)
+ expect(res.status).toBe(200)
+ })
+
+ it('allows `hanko-allow-all` with global middleware', async () => {
+ const res = await fetch('/about', authenticatedFetchOptions)
+ expect(res.status).toBe(200)
+ })
+})