Skip to content

Commit 07144fb

Browse files
authored
feat(auth): add redirects.logout (#173)
1 parent 0ebb0ba commit 07144fb

File tree

7 files changed

+66
-7
lines changed

7 files changed

+66
-7
lines changed

docs/content/1.getting-started/2.configuration.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default defineNuxtConfig({
1313
redirects: {
1414
login: '/login',
1515
guest: '/',
16+
// logout: '/goodbye', // optional
1617
},
1718
preserveRedirect: true,
1819
redirectQueryKey: 'redirect',
@@ -50,12 +51,13 @@ export default defineNuxtConfig({
5051
Path to the client auth config file, relative to the project root.
5152
::
5253

53-
::field{name="redirects" type="{ login?: string, guest?: string }"}
54+
::field{name="redirects" type="{ login?: string, guest?: string, logout?: string }"}
5455
Default: `{ login: '/login', guest: '/' }`
5556

5657
Global redirect fallbacks:
5758
- `login`: where to redirect unauthenticated users
5859
- `guest`: where to redirect authenticated users trying to access guest-only routes
60+
- `logout`: where to navigate after logout (no default)
5961

6062
Per-route `redirectTo` takes precedence when set.
6163
::

docs/content/5.api/1.composables.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ Signs the user out and clears the local session state.
7777
await signOut()
7878
```
7979

80+
If `auth.redirects.logout` is configured, `signOut()` will navigate there automatically (client-side), unless you provide `onSuccess`.
81+
8082
**Options**
8183

8284
```ts

src/module/runtime.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ export function setupRuntimeConfig(input: SetupRuntimeConfigInput): { useHubKV:
4141
nuxt.options.runtimeConfig.public.siteUrl = process.env.NUXT_PUBLIC_SITE_URL
4242

4343
nuxt.options.runtimeConfig.public.auth = defu(nuxt.options.runtimeConfig.public.auth as Record<string, unknown>, {
44-
redirects: { login: options.redirects?.login ?? '/login', guest: options.redirects?.guest ?? '/' },
44+
redirects: {
45+
login: options.redirects?.login ?? '/login',
46+
guest: options.redirects?.guest ?? '/',
47+
logout: options.redirects?.logout,
48+
},
4549
preserveRedirect: options.preserveRedirect ?? true,
4650
redirectQueryKey: options.redirectQueryKey ?? 'redirect',
4751
useDatabase: databaseProvider !== 'none',

src/runtime/app/composables/useUserSession.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AppAuthClient, AuthSession, AuthUser } from '#nuxt-better-auth'
22
import type { ComputedRef, Ref } from 'vue'
33
import createAppAuthClient from '#auth/client'
4-
import { computed, nextTick, useNuxtApp, useRequestHeaders, useRequestURL, useRuntimeConfig, useState, watch } from '#imports'
4+
import { computed, navigateTo, nextTick, useNuxtApp, useRequestHeaders, useRequestURL, useRuntimeConfig, useState, watch } from '#imports'
55
import { normalizeAuthActionError } from '../internal/auth-action-error'
66

77
export interface SignOutOptions { onSuccess?: () => void | Promise<void> }
@@ -311,8 +311,15 @@ export function useUserSession(): UseUserSessionReturn {
311311
throw new Error('signOut can only be called on client-side')
312312
await client.signOut()
313313
clearSession()
314-
if (options?.onSuccess)
314+
if (options?.onSuccess) {
315315
await options.onSuccess()
316+
return
317+
}
318+
319+
const authConfig = runtimeConfig.public.auth as { redirects?: { logout?: string } } | undefined
320+
const logoutRedirect = authConfig?.redirects?.logout
321+
if (logoutRedirect)
322+
await navigateTo(logoutRedirect)
316323
}
317324

318325
return {

src/runtime/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export interface BetterAuthModuleOptions {
3434
login?: string
3535
/** Where to redirect authenticated users on guest-only routes. Default: '/' */
3636
guest?: string
37+
/** Where to navigate after logout. Default: no automatic navigation */
38+
logout?: string
3739
}
3840
/**
3941
* When redirecting unauthenticated users to the login route, append a query param
@@ -76,7 +78,7 @@ export interface BetterAuthModuleOptions {
7678

7779
// Runtime config type for public.auth
7880
export interface AuthRuntimeConfig {
79-
redirects: { login: string, guest: string }
81+
redirects: { login: string, guest: string, logout?: string }
8082
preserveRedirect: boolean
8183
redirectQueryKey: string
8284
useDatabase: boolean

src/runtime/server/api/_better-auth/config.get.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default defineEventHandler(async (event) => {
99
const authContext = await ((auth as { $context?: Promise<{ trustedOrigins?: string[] }> | { trustedOrigins?: string[] } }).$context)
1010
const runtimeConfig = useRuntimeConfig()
1111
const publicAuth = runtimeConfig.public?.auth as {
12-
redirects?: { login?: string, guest?: string }
12+
redirects?: { login?: string, guest?: string, logout?: string }
1313
preserveRedirect?: boolean
1414
redirectQueryKey?: string
1515
useDatabase?: boolean
@@ -28,7 +28,11 @@ export default defineEventHandler(async (event) => {
2828
config: {
2929
// Module config (nuxt.config.ts)
3030
module: {
31-
redirects: { login: publicAuth?.redirects?.login ?? '/login', guest: publicAuth?.redirects?.guest ?? '/' },
31+
redirects: {
32+
login: publicAuth?.redirects?.login ?? '/login',
33+
guest: publicAuth?.redirects?.guest ?? '/',
34+
logout: publicAuth?.redirects?.logout,
35+
},
3236
preserveRedirect: publicAuth?.preserveRedirect ?? true,
3337
redirectQueryKey: publicAuth?.redirectQueryKey ?? 'redirect',
3438
hubSecondaryStorage: privateAuth?.hubSecondaryStorage ?? false,

test/use-user-session.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ const runtimeConfig = {
2121
session: {
2222
skipHydratedSsrGetSession: false,
2323
},
24+
redirects: {} as Record<string, unknown>,
2425
},
2526
},
2627
}
2728

2829
const requestURL = { origin: 'http://localhost:3000' }
2930
let requestHeaders: HeadersInit | undefined = { cookie: 'session=test' }
3031
const state = new Map<string, ReturnType<typeof ref>>()
32+
const navigateTo = vi.fn(async () => {})
3133

3234
const sessionAtom = ref<SessionState>({
3335
data: null,
@@ -55,6 +57,7 @@ vi.mock('#imports', async () => {
5557
const vue = await import('vue')
5658
return {
5759
computed: vue.computed,
60+
navigateTo,
5861
nextTick: vue.nextTick,
5962
watch: vue.watch,
6063
useNuxtApp: () => ({ payload }),
@@ -96,6 +99,8 @@ describe('useUserSession hydration bootstrap', () => {
9699
requestURL.origin = 'http://localhost:3000'
97100
runtimeConfig.public.siteUrl = 'http://localhost:3000'
98101
runtimeConfig.public.auth.session.skipHydratedSsrGetSession = false
102+
runtimeConfig.public.auth.redirects = {}
103+
navigateTo.mockClear()
99104

100105
sessionAtom.value = {
101106
data: null,
@@ -280,4 +285,37 @@ describe('useUserSession hydration bootstrap', () => {
280285
expect(auth.session.value).toEqual({ id: 'session-3', ipAddress: '127.0.0.1' })
281286
expect(auth.user.value).toEqual({ id: 'user-3', email: 'user3@example.com' })
282287
})
288+
289+
it('signOut navigates to redirects.logout when configured (and no onSuccess)', async () => {
290+
runtimeConfig.public.auth.redirects = { logout: '/logged-out' }
291+
292+
const useUserSession = await loadUseUserSession()
293+
const auth = useUserSession()
294+
await auth.signOut()
295+
296+
expect(navigateTo).toHaveBeenCalledWith('/logged-out')
297+
})
298+
299+
it('signOut does not auto-navigate when onSuccess is provided', async () => {
300+
runtimeConfig.public.auth.redirects = { logout: '/logged-out' }
301+
302+
const useUserSession = await loadUseUserSession()
303+
const auth = useUserSession()
304+
305+
const onSuccess = vi.fn()
306+
await auth.signOut({ onSuccess })
307+
308+
expect(onSuccess).toHaveBeenCalledOnce()
309+
expect(navigateTo).not.toHaveBeenCalled()
310+
})
311+
312+
it('signOut does not auto-navigate when redirects.logout is not configured', async () => {
313+
runtimeConfig.public.auth.redirects = {}
314+
315+
const useUserSession = await loadUseUserSession()
316+
const auth = useUserSession()
317+
await auth.signOut()
318+
319+
expect(navigateTo).not.toHaveBeenCalled()
320+
})
283321
})

0 commit comments

Comments
 (0)