GlobalMiddleware: allow all
+
+definePageMeta({
+ hanko: {
+ allow: 'all',
+ }
+})
+
+definePageMeta({
+ hanko: {
+ allow: 'all',
+ }
+})
+
+definePageMeta({
+ hanko: {
+ allow: 'logged-in',
+ }
+})
+
+definePageMeta({
+ hanko: {
+ allow: 'logged-out',
+ }
+})
+
+definePageMeta({
+ hanko: {
+ deny: 'logged-in',
+ }
+})
+
+definePageMeta({
+ hanko: {
+ deny: 'logged-out',
+ }
+})
+ In case you do, the middleware takes precedence.
+- Only logged out users can see this page
definePageMeta({
+
+ Only logged out users can see this page
+
+definePageMeta({
middleware: ['hanko-logged-out'],
})
-
+
- You were redirected here from {{ $route.query.redirect }}, once you login, you'll be sent back automatically!
+ You were redirected here from {{ $route.query.redirect }}, once you login, you'll be sent back
+ automatically!
- Only logged in users can see this page
definePageMeta({
+
+ Only logged in users can see this page
+
+definePageMeta({
middleware: ['hanko-logged-in'],
})
-
+
diff --git a/playground/pages/user.vue b/playground/pages/user.vue
index d1739f53..ed4c6a92 100644
--- a/playground/pages/user.vue
+++ b/playground/pages/user.vue
@@ -19,11 +19,13 @@ async function tryAuthenticatedRequest() {
You are logged in!
-
- Only logged in users can see this page
definePageMeta({
+
+ Only logged in users can see this page
+
+definePageMeta({
middleware: ['hanko-logged-in'],
})
-
+
diff --git a/src/module.ts b/src/module.ts
index 87823a5d..19e2ccf7 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -10,6 +10,7 @@ export interface ModuleOptions {
apiURL?: string
registerComponents?: boolean
augmentContext?: boolean
+ globalMiddleware?: boolean
cookieName?: string
cookieSameSite?: CookieSameSite
cookieDomain?: string
@@ -40,6 +41,7 @@ export default defineNuxtModule({
apiURL: '',
registerComponents: true,
augmentContext: true,
+ globalMiddleware: false,
cookieName: 'hanko',
redirects: {
login: '/login',
@@ -68,6 +70,7 @@ export default defineNuxtModule({
nuxt.options.appConfig = defu(nuxt.options.appConfig, {
hanko: {
redirects: options.redirects,
+ globalMiddleware: options.globalMiddleware,
},
})
@@ -83,6 +86,19 @@ export default defineNuxtModule({
})
}
+ if (options.globalMiddleware) {
+ addRouteMiddleware({
+ name: 'hanko-global-logged-in',
+ path: resolver.resolve('./runtime/middleware/global-logged-in'),
+ global: true,
+ })
+ nuxt.hook('prepare:types', ({ references }) => {
+ references.push({
+ path: resolver.resolve('./page-meta-global.d.ts'),
+ })
+ })
+ }
+
if (options.augmentContext) {
addServerHandler({
middleware: true,
@@ -107,6 +123,14 @@ export default defineNuxtModule({
from: resolver.resolve('./runtime/composables/index'),
imports: ['useHanko'],
})
+ addImportsSources({
+ from: resolver.resolve('./runtime/middleware/logged-in'),
+ imports: ['hankoLoggedIn'],
+ })
+ addImportsSources({
+ from: resolver.resolve('./runtime/middleware/logged-out'),
+ imports: ['hankoLoggedOut'],
+ })
const hankoElementsTemplate = addTemplate({
filename: 'hanko-elements.mjs',
diff --git a/src/page-meta-global.d.ts b/src/page-meta-global.d.ts
new file mode 100644
index 00000000..de67b12e
--- /dev/null
+++ b/src/page-meta-global.d.ts
@@ -0,0 +1,22 @@
+/**
+ * PageMeta augmentation for the hanko `globalMiddleware` feature.
+ * This file must live outside `src/runtime/` because Nuxt automatically
+ * includes every module's `dist/runtime` in the app's TypeScript project.
+ * If this augmentation were in runtime/, it would always be loaded and
+ * `hanko` would appear on PageMeta even when globalMiddleware is false.
+ * By living here, it is only loaded when we add it via prepare:types
+ * (which we do only when globalMiddleware is true).
+ */
+type HankoMeta
+ = | { allow?: 'all' | 'logged-in' | 'logged-out', deny?: never }
+ | { allow?: never, deny?: 'logged-in' | 'logged-out' }
+
+declare module 'nuxt/app' {
+ interface PageMeta {
+ hanko?: HankoMeta
+ }
+}
+
+// Required: makes this file a module so `declare module 'nuxt/app'` is treated
+// as an augmentation (merge) rather than a new module declaration.
+export {}
diff --git a/src/runtime/middleware/global-logged-in.ts b/src/runtime/middleware/global-logged-in.ts
new file mode 100644
index 00000000..e7a31e1f
--- /dev/null
+++ b/src/runtime/middleware/global-logged-in.ts
@@ -0,0 +1,45 @@
+import { defineNuxtRouteMiddleware, hankoLoggedIn, hankoLoggedOut } 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
+
+ // Don't trigger on client-side navigation on the same-page
+ // (changes in to.query or to.hash)
+ if (import.meta.client && to.path === from.path) return
+
+ // If a hanko middleware is explicitly set, that middleware handles
+ // navigation and the default hankoLoggedIn is skipped
+ if ([to.meta.middleware].flat().some(isHankoMiddleware)) return
+
+ // Next it respects custom hanko PageMeta
+ if (to.meta.hanko) {
+ const { allow, deny } = to.meta.hanko
+
+ if (allow == 'all') return
+ else if (allow == 'logged-in') return await hankoLoggedIn(to)
+ else if (allow == 'logged-out') return await hankoLoggedOut(to)
+ else if (deny == 'logged-in') return await hankoLoggedOut(to)
+ else if (deny == 'logged-out') return await hankoLoggedIn(to)
+ }
+
+ // If no hanko middleware is set, default to hankoLoggedIn
+ return await hankoLoggedIn(to)
+})
+
+/**
+ * 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`
+ */
+const isHankoMiddleware = (middleware: unknown) => {
+ return (
+ typeof middleware === 'string'
+ && ['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 bd79eed1..87327e48 100644
--- a/src/runtime/middleware/logged-in.ts
+++ b/src/runtime/middleware/logged-in.ts
@@ -1,8 +1,9 @@
import { withQuery } from 'ufo'
+import type { RouteMiddleware } from '#app'
import { defineNuxtRouteMiddleware, navigateTo, useRouter, useAppConfig, useHanko, useRequestEvent } from '#imports'
import type {} from 'nuxt/app'
-export default defineNuxtRouteMiddleware(async (to) => {
+export const hankoLoggedIn = (async (to) => {
const redirects = useAppConfig().hanko.redirects
if (import.meta.server) {
@@ -27,4 +28,6 @@ export default defineNuxtRouteMiddleware(async (to) => {
removeHankoHook()
removeRouterHook()
})
-})
+}) satisfies RouteMiddleware
+
+export default defineNuxtRouteMiddleware(hankoLoggedIn)
diff --git a/src/runtime/middleware/logged-out.ts b/src/runtime/middleware/logged-out.ts
index e569ca6e..dafbb432 100644
--- a/src/runtime/middleware/logged-out.ts
+++ b/src/runtime/middleware/logged-out.ts
@@ -1,6 +1,7 @@
+import type { RouteMiddleware } from '#app'
import { defineNuxtRouteMiddleware, navigateTo, useRouter, useAppConfig, useHanko, useRequestEvent } from '#imports'
-export default defineNuxtRouteMiddleware(async (to) => {
+export const hankoLoggedOut = (async (to) => {
const redirects = useAppConfig().hanko.redirects
if (import.meta.server) {
@@ -29,4 +30,6 @@ export default defineNuxtRouteMiddleware(async (to) => {
removeHankoHook()
removeRouterHook()
})
-})
+}) satisfies RouteMiddleware
+
+export default defineNuxtRouteMiddleware(hankoLoggedOut)
diff --git a/test/basic.test.ts b/test/basic.test.ts
deleted file mode 100644
index b88b50e9..00000000
--- a/test/basic.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { fileURLToPath } from 'node:url'
-import { describe, it, expect } from 'vitest'
-import { setup, fetch, $fetch } from '@nuxt/test-utils'
-
-await setup({
- rootDir: fileURLToPath(new URL('../playground', import.meta.url)),
-})
-
-describe('ssr', async () => {
- it('redirects to the login page', async () => {
- const res = await fetch('/protected', { redirect: 'manual' })
- expect(res.headers.get('location')).toBe('/login?redirect=/protected')
- })
-
- it('respects custom elements', async () => {
- const html = await $fetch('/login')
- expect(html).toContain(' ')
- })
-})
diff --git a/test/client/basic-loggedIn-client.test.ts b/test/client/basic-loggedIn-client.test.ts
new file mode 100644
index 00000000..ae482374
--- /dev/null
+++ b/test/client/basic-loggedIn-client.test.ts
@@ -0,0 +1,33 @@
+import { fileURLToPath } from 'node:url'
+import { setup, createPage, useTestContext } from '@nuxt/test-utils'
+import defu from 'defu'
+import { describe, expect, it } from 'vitest'
+import { mockLoggedIn } from '../config'
+
+await setup({
+ rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
+ nuxtConfig: defu(mockLoggedIn),
+})
+
+describe('No global middleware, logged in, client-side', { timeout: 20_000 }, async () => {
+ it('renders page with no middleware', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('text=About page')
+ await page.waitForURL(`${useTestContext().url}about`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}about`)
+ })
+
+ it('renders page with hanko-logged-in middleware', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('text=Protected page 🔐')
+ await page.waitForURL(`${useTestContext().url}protected`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}protected`)
+ })
+
+ it('redirects to home page for hanko-logged-out middleware', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('text=Log in page ⛔️👤')
+ await page.waitForURL(`${useTestContext().url}`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}`)
+ })
+})
diff --git a/test/client/basic-loggedOut-client.test.ts b/test/client/basic-loggedOut-client.test.ts
new file mode 100644
index 00000000..9fcb8daa
--- /dev/null
+++ b/test/client/basic-loggedOut-client.test.ts
@@ -0,0 +1,37 @@
+import { fileURLToPath } from 'node:url'
+import { setup, createPage, useTestContext } from '@nuxt/test-utils'
+import { describe, expect, it } from 'vitest'
+
+await setup({
+ rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
+})
+
+describe('No global middleware, not logged in, client-side', { timeout: 20_000 }, async () => {
+ it('pageMeta:hanko has no effect without global middleware', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('#allow-logged-in')
+ await page.waitForURL(`${useTestContext().url}global/allow/logged-in`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}global/allow/logged-in`)
+ })
+
+ it('renders page with no middleware', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('text=About page')
+ await page.waitForURL(`${useTestContext().url}about`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}about`)
+ })
+
+ it('redirects to login page for hanko-logged-in middleware', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('text=Protected page 🔐')
+ await page.waitForURL(`${useTestContext().url}login?redirect=/protected`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}login?redirect=/protected`)
+ })
+
+ it('renders page with hanko-logged-out middleware', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('text=Log in page ⛔️👤')
+ await page.waitForURL(`${useTestContext().url}login`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}login`)
+ })
+})
diff --git a/test/client/globalMiddleware-loggedIn-client.test.ts b/test/client/globalMiddleware-loggedIn-client.test.ts
new file mode 100644
index 00000000..7546f3df
--- /dev/null
+++ b/test/client/globalMiddleware-loggedIn-client.test.ts
@@ -0,0 +1,68 @@
+import { fileURLToPath } from 'node:url'
+import { setup, createPage, useTestContext } from '@nuxt/test-utils'
+import defu from 'defu'
+import { describe, expect, it } from 'vitest'
+import { enableGlobalMiddleware, mockLoggedIn } from '../config'
+
+await setup({
+ rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
+ nuxtConfig: defu(mockLoggedIn, enableGlobalMiddleware),
+})
+
+describe('Global middleware, logged in, client-side', { timeout: 20_000 }, async () => {
+ it('renders page with no middleware', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('text=About page')
+ await page.waitForURL(`${useTestContext().url}about`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}about`)
+ })
+
+ it('renders page with hanko-logged-in middleware', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('text=Protected page 🔐')
+ await page.waitForURL(`${useTestContext().url}protected`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}protected`)
+ })
+
+ it('redirects to home page for hanko-logged-out middleware', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('text=Log in page ⛔️👤')
+ await page.waitForURL(`${useTestContext().url}`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}`)
+ })
+
+ it('allow:all renders page', async () => {
+ const page = await createPage('/about')
+ await page.click('#allow-all')
+ await page.waitForURL(`${useTestContext().url}global/allow/all`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}global/allow/all`)
+ })
+
+ it('allow:logged-in renders page', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('#allow-logged-in')
+ await page.waitForURL(`${useTestContext().url}global/allow/logged-in`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}global/allow/logged-in`)
+ })
+
+ it('allow:logged-out redirects to home page', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('#allow-logged-out')
+ await page.waitForURL(`${useTestContext().url}`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}`)
+ })
+
+ it('deny:logged-in redirects to home page', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('#deny-logged-in')
+ await page.waitForURL(`${useTestContext().url}`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}`)
+ })
+
+ it('deny:logged-out renders page', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('#deny-logged-out')
+ await page.waitForURL(`${useTestContext().url}global/deny/logged-out`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}global/deny/logged-out`)
+ })
+})
diff --git a/test/client/globalMiddleware-loggedOut-client.test.ts b/test/client/globalMiddleware-loggedOut-client.test.ts
new file mode 100644
index 00000000..2b728645
--- /dev/null
+++ b/test/client/globalMiddleware-loggedOut-client.test.ts
@@ -0,0 +1,81 @@
+import { fileURLToPath } from 'node:url'
+import { setup, createPage, useTestContext } from '@nuxt/test-utils'
+import defu from 'defu'
+import { describe, expect, it } from 'vitest'
+import { enableGlobalMiddleware } from '../config'
+
+await setup({
+ rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
+ nuxtConfig: defu(enableGlobalMiddleware),
+})
+
+describe('Global middleware, not logged in, client-side', { timeout: 20_000 }, async () => {
+ it('redirects to login for page without explicit middleware', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('text=About page')
+ await page.waitForURL(`${useTestContext().url}login?redirect=/about`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}login?redirect=/about`)
+ })
+
+ it('hanko-logged-in middleware redirects to the login page', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('text=Protected page 🔐')
+ await page.waitForURL(`${useTestContext().url}login?redirect=/protected`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}login?redirect=/protected`)
+ })
+
+ it('hanko-logged-out middleware renders page', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('text=Log in page ⛔️👤')
+ await page.waitForURL(`${useTestContext().url}login`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}login`)
+ })
+
+ it('allow:all renders page', async () => {
+ const page = await createPage('/login')
+ await page.click('#allow-all')
+ await page.waitForURL(`${useTestContext().url}global/allow/all`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}global/allow/all`)
+ })
+
+ it('allow:logged-in redirects to login', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('#allow-logged-in')
+ await page.waitForURL(`${useTestContext().url}login?redirect=/global/allow/logged-in`, {
+ timeout: 4000,
+ })
+ expect(page.url()).toBe(`${useTestContext().url}login?redirect=/global/allow/logged-in`)
+ })
+
+ it('allow:logged-out renders page', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('#allow-logged-out')
+ await page.waitForURL(`${useTestContext().url}global/allow/logged-out`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}global/allow/logged-out`)
+ })
+
+ it('deny:logged-in renders page', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('#deny-logged-in')
+ await page.waitForURL(`${useTestContext().url}global/deny/logged-in`, { timeout: 4000 })
+ expect(page.url()).toBe(`${useTestContext().url}global/deny/logged-in`)
+ })
+
+ it('deny:logged-out redirects to login page', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('#deny-logged-out')
+ await page.waitForURL(`${useTestContext().url}login?redirect=/global/deny/logged-out`, {
+ timeout: 4000,
+ })
+ expect(page.url()).toBe(`${useTestContext().url}login?redirect=/global/deny/logged-out`)
+ })
+
+ it('applies middleware over pageMeta', async () => {
+ const page = await createPage('/global/allow/all')
+ await page.click('#incorrect-usage')
+ await page.waitForURL(`${useTestContext().url}login?redirect=/global/incorrect-usage`, {
+ timeout: 4000,
+ })
+ expect(page.url()).toBe(`${useTestContext().url}login?redirect=/global/incorrect-usage`)
+ })
+})
diff --git a/test/config.ts b/test/config.ts
new file mode 100644
index 00000000..46beef3e
--- /dev/null
+++ b/test/config.ts
@@ -0,0 +1,25 @@
+import { addImports, createResolver } from '@nuxt/kit'
+import type { NuxtConfig } from 'nuxt/schema'
+
+export const enableGlobalMiddleware = {
+ hanko: { globalMiddleware: true },
+} satisfies NuxtConfig
+
+const resolver = createResolver(import.meta.url)
+
+export const mockLoggedIn = {
+ hooks: {
+ ready: () => {
+ addImports({
+ name: 'useHanko',
+ as: 'useHanko',
+ from: resolver.resolve('./mocks/useHanko.ts'),
+ })
+ addImports({
+ name: 'useRequestEvent',
+ as: 'useRequestEvent',
+ from: resolver.resolve('./mocks/useRequestEvent.ts'),
+ })
+ },
+ },
+} satisfies NuxtConfig
diff --git a/test/mocks/useHanko.ts b/test/mocks/useHanko.ts
new file mode 100644
index 00000000..b5386163
--- /dev/null
+++ b/test/mocks/useHanko.ts
@@ -0,0 +1,6 @@
+export const useHanko = () => ({
+ user: {
+ getCurrent: async () => ({ id: 'some-user-id', webauthn_credentials: [] }),
+ },
+ onUserLoggedOut: (_: () => void) => {},
+})
diff --git a/test/mocks/useRequestEvent.ts b/test/mocks/useRequestEvent.ts
new file mode 100644
index 00000000..e9f08c3d
--- /dev/null
+++ b/test/mocks/useRequestEvent.ts
@@ -0,0 +1,17 @@
+import type { H3EventContext } from 'h3'
+
+export const useRequestEvent = () => ({
+ context: {
+ hanko: {
+ aud: [],
+ email: {
+ address: 'mail@example.com',
+ is_primary: true,
+ is_verified: true,
+ },
+ exp: 0,
+ iat: 0,
+ sub: 'some-user-id',
+ },
+ } satisfies H3EventContext,
+})
diff --git a/test/ssr/basic-loggedIn-ssr.test.ts b/test/ssr/basic-loggedIn-ssr.test.ts
new file mode 100644
index 00000000..f834da0d
--- /dev/null
+++ b/test/ssr/basic-loggedIn-ssr.test.ts
@@ -0,0 +1,27 @@
+import { fileURLToPath } from 'node:url'
+import { fetch, setup } from '@nuxt/test-utils'
+import defu from 'defu'
+import { describe, expect, it } from 'vitest'
+import { mockLoggedIn } from '../config'
+
+await setup({
+ rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
+ nuxtConfig: defu(mockLoggedIn),
+})
+
+describe('No global middleware, logged in, ssr', async () => {
+ it('renders page without middleware', async () => {
+ const res = await fetch('/about', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+
+ it('hanko-logged-in middleware renders page', async () => {
+ const res = await fetch('/protected', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+
+ it('hanko-logged-out middleware redirects to the home page', async () => {
+ const res = await fetch('/login', { redirect: 'manual' })
+ expect(res.headers.get('location')).toBe('/')
+ })
+})
diff --git a/test/ssr/basic-loggedOut-ssr.test.ts b/test/ssr/basic-loggedOut-ssr.test.ts
new file mode 100644
index 00000000..e7cba2dc
--- /dev/null
+++ b/test/ssr/basic-loggedOut-ssr.test.ts
@@ -0,0 +1,34 @@
+import { fileURLToPath } from 'node:url'
+import { describe, it, expect } from 'vitest'
+import { setup, fetch, $fetch } from '@nuxt/test-utils'
+
+await setup({
+ rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
+})
+
+describe('No global middleware, not logged in, ssr', async () => {
+ it('respects custom elements', async () => {
+ const html = await $fetch('/login')
+ expect(html).toContain(' ')
+ })
+
+ it('pageMeta:hanko has no effect without global middleware', async () => {
+ const res = await fetch('/global/allow/logged-in', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+
+ it('hanko-logged-in middleware redirects to the login page', async () => {
+ const res = await fetch('/protected', { redirect: 'manual' })
+ expect(res.headers.get('location')).toBe('/login?redirect=/protected')
+ })
+
+ it('hanko-logged-out middleware renders page', async () => {
+ const res = await fetch('/login', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+
+ it('renders page without middleware', async () => {
+ const res = await fetch('/about', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+})
diff --git a/test/ssr/globalMiddleware-loggedIn-ssr.test.ts b/test/ssr/globalMiddleware-loggedIn-ssr.test.ts
new file mode 100644
index 00000000..498d1b00
--- /dev/null
+++ b/test/ssr/globalMiddleware-loggedIn-ssr.test.ts
@@ -0,0 +1,52 @@
+import { fileURLToPath } from 'node:url'
+import { fetch, setup } from '@nuxt/test-utils'
+import defu from 'defu'
+import { describe, expect, it } from 'vitest'
+import { enableGlobalMiddleware, mockLoggedIn } from '../config'
+
+await setup({
+ rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
+ nuxtConfig: defu(mockLoggedIn, enableGlobalMiddleware),
+})
+
+describe('Global middleware, logged in, ssr', async () => {
+ it('hanko-logged-in middleware renders page', async () => {
+ const res = await fetch('/protected', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+
+ it('hanko-logged-out middleware redirects to home page', async () => {
+ const res = await fetch('/login', { redirect: 'manual' })
+ expect(res.headers.get('location')).toBe('/')
+ })
+
+ it('renders page without explicit middleware', async () => {
+ const res = await fetch('/about', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+
+ it('allow:all renders page', async () => {
+ const res = await fetch('/global/allow/all', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+
+ it('allow:logged-in renders page', async () => {
+ const res = await fetch('/global/allow/logged-in', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+
+ it('allow:logged-out redirects to home page', async () => {
+ const res = await fetch('/global/allow/logged-out', { redirect: 'manual' })
+ expect(res.headers.get('location')).toBe('/')
+ })
+
+ it('deny:logged-in redirects to home page', async () => {
+ const res = await fetch('/global/deny/logged-in', { redirect: 'manual' })
+ expect(res.headers.get('location')).toBe('/')
+ })
+
+ it('deny:logged-out renders page', async () => {
+ const res = await fetch('/global/deny/logged-out', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+})
diff --git a/test/ssr/globalMiddleware-loggedOut-ssr.test.ts b/test/ssr/globalMiddleware-loggedOut-ssr.test.ts
new file mode 100644
index 00000000..7c6fd5f2
--- /dev/null
+++ b/test/ssr/globalMiddleware-loggedOut-ssr.test.ts
@@ -0,0 +1,57 @@
+import { fileURLToPath } from 'node:url'
+import { fetch, setup } from '@nuxt/test-utils'
+import defu from 'defu'
+import { describe, expect, it } from 'vitest'
+import { enableGlobalMiddleware } from '../config'
+
+await setup({
+ rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
+ nuxtConfig: defu(enableGlobalMiddleware),
+})
+
+describe('Global middleware, not logged in, ssr', async () => {
+ it('hanko-logged-in middleware redirects to the login page', async () => {
+ const res = await fetch('/protected', { redirect: 'manual' })
+ expect(res.headers.get('location')).toBe('/login?redirect=/protected')
+ })
+
+ it('hanko-logged-out middleware renders page', async () => {
+ const res = await fetch('/login', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+
+ it('redirects to login for page without explicit middleware', async () => {
+ const res = await fetch('/about', { redirect: 'manual' })
+ expect(res.headers.get('location')).toBe('/login?redirect=/about')
+ })
+
+ it('allow:all renders page', async () => {
+ const res = await fetch('/global/allow/all', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+
+ it('allow:logged-in redirects to login', async () => {
+ const res = await fetch('/global/allow/logged-in', { redirect: 'manual' })
+ expect(res.headers.get('location')).toBe('/login?redirect=/global/allow/logged-in')
+ })
+
+ it('allow:logged-out renders page', async () => {
+ const res = await fetch('/global/allow/logged-out', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+
+ it('deny:logged-in renders page', async () => {
+ const res = await fetch('/global/deny/logged-in', { redirect: 'manual' })
+ expect(res.redirected).toBeFalsy()
+ })
+
+ it('deny:logged-out redirects to login', async () => {
+ const res = await fetch('/global/deny/logged-out', { redirect: 'manual' })
+ expect(res.headers.get('location')).toBe('/login?redirect=/global/deny/logged-out')
+ })
+
+ it('applies middleware over pageMeta', async () => {
+ const res = await fetch('/global/incorrect-usage', { redirect: 'manual' })
+ expect(res.headers.get('location')).toBe('/login?redirect=/global/incorrect-usage')
+ })
+})
diff --git a/vitest.config.ts b/vitest.config.ts
index 6df4be54..943ca8ab 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -3,5 +3,6 @@ import { defineConfig, configDefaults } from 'vitest/config'
export default defineConfig({
test: {
exclude: ['**/test.*', ...configDefaults.exclude],
+ coverage: { include: ['src/**'] },
},
})