diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 3742aa54..76e36418 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -129,6 +129,8 @@ jobs:
# Check building
- run: pnpm build
+ env:
+ PORT: 3001
# start prod-app and curl from it
- run: "timeout 60 pnpm start & (sleep 45 && curl --fail localhost:$PORT)"
diff --git a/docs/content/1.getting-started/4.caching-content.md b/docs/content/1.getting-started/4.caching-content.md
new file mode 100644
index 00000000..ae49a3cd
--- /dev/null
+++ b/docs/content/1.getting-started/4.caching-content.md
@@ -0,0 +1,85 @@
+---
+description: "Learn how to configure your project to support caching"
+---
+
+::alert{type="info"}
+If you are using the following routeRules (`swr`, `isr`, `prerender`), you will need to read this. When prerendering your entire site using `nuxi generate`, this is done automatically.
+::
+
+# Caching Content
+
+Often hosting providers offer caching on the edge. Most websites can experience incredible speeds (and cost savings) by taking advantage of caching. No cold start, no processing requests, no parsing Javascript... just HTML served immediately from a CDN.
+
+By default we send the user's authentication data down to the client in the HTML. This might not be ideal if you're caching your pages. Users may be able to see other user's authentication data if not handled properly.
+
+To add caching to your Nuxt app, follow the [Nuxt documentation on hybrid rendering](https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering).
+
+## Configuration
+
+::alert{type="warning"}
+If you find yourself needing to server-rendered auth methods like `getProviders()`, you must set the `baseURL` option on the `auth` object. This applies in development too.
+::
+
+### Page Specific Cache Rules
+
+If only a few of your pages are cached. Head over to the Nuxt config `routeRules`, add the `auth` key to your cached routes. Set `disableServerSideAuth` to `true`.
+
+```ts
+export default defineNuxtConfig({
+ modules: ['@sidebase/nuxt-auth'],
+ auth: {
+ // Optional - Needed for getProviders() method to work server-side
+ baseURL: 'http://localhost:3000',
+ },
+ routeRules: {
+ '/': {
+ swr: 86400000,
+ auth: {
+ disableServerSideAuth: true,
+ },
+ },
+ },
+})
+```
+
+### Module Cache Rules
+
+If all/most pages on your site are cached. Head over to the Nuxt config, add the `auth` key if not already there. Set `disableServerSideAuth` to `true`.
+
+```ts
+export default defineNuxtConfig({
+ modules: ['@sidebase/nuxt-auth'],
+ auth: {
+ disableServerSideAuth: true,
+ // Optional - Needed for getProviders() method to work server-side
+ baseURL: 'http://localhost:3000',
+ },
+})
+```
+
+### Combining Configurations
+
+Route-configured options take precedent over module-configured options. If you disabled server side auth in the module, you may still enable server side auth back by setting `auth.disableServerSideAuth` to `false`.
+
+For example: It may be ideal to add caching to every page besides your profile page.
+
+```ts
+export default defineNuxtConfig({
+ modules: ['@sidebase/nuxt-auth'],
+ auth: {
+ disableServerSideAuth: true,
+ },
+ routeRules: {
+ // Server side auth is disabled on this page because of global setting
+ '/': {
+ swr: 86400000,
+ }
+ // Server side auth is enabled on this page - route rules takes priority.
+ '/profile': {
+ auth: {
+ disableServerSideAuth: false,
+ },
+ },
+ },
+})
+```
diff --git a/docs/content/1.getting-started/4.getting-help.md b/docs/content/1.getting-started/5.getting-help.md
similarity index 100%
rename from docs/content/1.getting-started/4.getting-help.md
rename to docs/content/1.getting-started/5.getting-help.md
diff --git a/docs/content/2.configuration/2.nuxt-config.md b/docs/content/2.configuration/2.nuxt-config.md
index 738460f2..722ab665 100644
--- a/docs/content/2.configuration/2.nuxt-config.md
+++ b/docs/content/2.configuration/2.nuxt-config.md
@@ -16,6 +16,14 @@ interface ModuleOptions {
* Whether the module is enabled at all
*/
isEnabled?: boolean
+ /**
+ * Forces your server to send a "loading" authentication status on all requests, thus prompting the client to do a fetch. If your website has caching, this prevents the server from caching someone's authentication status.
+ *
+ * This effects the entire site, for route-specific rules, add `disableServerSideAuth` on `routeRules`.
+ *
+ * @default false
+ */
+ disableServerSideAuth?: boolean;
/**
* Full url at which the app will run combined with the path to authentication. You can set this differently depending on your selected authentication-provider:
* - `authjs`: You must set the full URL, with origin and path in production. You can leave this empty in development
diff --git a/docs/content/2.configuration/3.route-config.md b/docs/content/2.configuration/3.route-config.md
new file mode 100644
index 00000000..6a1fb965
--- /dev/null
+++ b/docs/content/2.configuration/3.route-config.md
@@ -0,0 +1,14 @@
+# Route Rules
+
+Use the `auth`-key inside the `nuxt.config.ts` `routeRules` to configure page-specific settings.
+
+```ts
+interface RouteOptions {
+ /**
+ * Forces your server to send a "loading" status on a route, prompting the client to fetch on the client. If a specific page has caching, this prevents the server from caching someone's authentication status.
+ *
+ * @default false
+ */
+ disableServerSideAuth: boolean;
+}
+```
diff --git a/docs/content/2.configuration/3.nuxt-auth-handler.md b/docs/content/2.configuration/4.nuxt-auth-handler.md
similarity index 100%
rename from docs/content/2.configuration/3.nuxt-auth-handler.md
rename to docs/content/2.configuration/4.nuxt-auth-handler.md
diff --git a/playground-authjs/nuxt.config.ts b/playground-authjs/nuxt.config.ts
index 025f351f..050090e2 100644
--- a/playground-authjs/nuxt.config.ts
+++ b/playground-authjs/nuxt.config.ts
@@ -6,6 +6,15 @@ export default defineNuxtConfig({
},
globalAppMiddleware: {
isEnabled: true
+ },
+ baseURL: `http://localhost:${process.env.PORT || 3000}`
+ },
+ routeRules: {
+ '/with-caching': {
+ swr: 86400000,
+ auth: {
+ disableServerSideAuth: true
+ }
}
}
})
diff --git a/playground-authjs/pages/index.vue b/playground-authjs/pages/index.vue
index 9d414476..5b9707ab 100644
--- a/playground-authjs/pages/index.vue
+++ b/playground-authjs/pages/index.vue
@@ -26,6 +26,10 @@ definePageMeta({ auth: false })
-> guest mode
+
+ -> cached page with swr
+
+
select one of the above actions to get started.
diff --git a/playground-authjs/pages/with-caching.vue b/playground-authjs/pages/with-caching.vue
new file mode 100644
index 00000000..0d7166fc
--- /dev/null
+++ b/playground-authjs/pages/with-caching.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
Server Render Time: {{ serverRenderTime?.toISOString() }}
+
Client Render Time: {{ clientRenderTime?.toISOString() }}
+
+
diff --git a/playground-local/nuxt.config.ts b/playground-local/nuxt.config.ts
index de98c89b..141c816d 100644
--- a/playground-local/nuxt.config.ts
+++ b/playground-local/nuxt.config.ts
@@ -27,5 +27,13 @@ export default defineNuxtConfig({
globalAppMiddleware: {
isEnabled: true
}
+ },
+ routeRules: {
+ '/with-caching': {
+ swr: 86400000,
+ auth: {
+ disableServerSideAuth: true
+ }
+ }
}
})
diff --git a/playground-local/pages/index.vue b/playground-local/pages/index.vue
index 9d414476..5b9707ab 100644
--- a/playground-local/pages/index.vue
+++ b/playground-local/pages/index.vue
@@ -26,6 +26,10 @@ definePageMeta({ auth: false })
-> guest mode
+
+ -> cached page with swr
+
+
select one of the above actions to get started.
diff --git a/playground-local/pages/with-caching.vue b/playground-local/pages/with-caching.vue
new file mode 100644
index 00000000..0d7166fc
--- /dev/null
+++ b/playground-local/pages/with-caching.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
Server Render Time: {{ serverRenderTime?.toISOString() }}
+
Client Render Time: {{ clientRenderTime?.toISOString() }}
+
+
diff --git a/playground-refresh/nuxt.config.ts b/playground-refresh/nuxt.config.ts
index 9ce3e9a7..7f5db4f7 100644
--- a/playground-refresh/nuxt.config.ts
+++ b/playground-refresh/nuxt.config.ts
@@ -27,5 +27,13 @@ export default defineNuxtConfig({
globalAppMiddleware: {
isEnabled: true
}
+ },
+ routeRules: {
+ '/with-caching': {
+ swr: 86400000,
+ auth: {
+ disableServerSideAuth: true
+ }
+ }
}
})
diff --git a/playground-refresh/pages/index.vue b/playground-refresh/pages/index.vue
index c60d174e..932c0119 100644
--- a/playground-refresh/pages/index.vue
+++ b/playground-refresh/pages/index.vue
@@ -25,6 +25,10 @@ definePageMeta({ auth: false })
-> guest mode
+
+ -> cached page with swr
+
+
select one of the above actions to get started.
diff --git a/playground-refresh/pages/with-caching.vue b/playground-refresh/pages/with-caching.vue
new file mode 100644
index 00000000..0d7166fc
--- /dev/null
+++ b/playground-refresh/pages/with-caching.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
Server Render Time: {{ serverRenderTime?.toISOString() }}
+
Client Render Time: {{ clientRenderTime?.toISOString() }}
+
+
diff --git a/src/module.ts b/src/module.ts
index c02f4384..722743fa 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -22,6 +22,7 @@ import type {
const topLevelDefaults = {
isEnabled: true,
+ disableServerSideAuth: false,
session: {
enableRefreshPeriodically: false,
enableRefreshOnWindowFocus: true
@@ -203,7 +204,16 @@ export default defineNuxtModule({
(options.provider as any).sessionDataType
)
: '',
- '}'
+ '}',
+ "declare module 'nitropack' {",
+ ' interface NitroRouteRules {',
+ ` auth?: import('${resolve('./runtime/types.ts')}').RouteOptions`,
+ ' }',
+ ' interface NitroRouteConfig {',
+ ` auth?: import('${resolve('./runtime/types.ts')}').RouteOptions`,
+ ' }',
+ '}',
+ 'export {}'
].join('\n')
})
diff --git a/src/runtime/composables/local/useAuth.ts b/src/runtime/composables/local/useAuth.ts
index 114248aa..73cf5898 100644
--- a/src/runtime/composables/local/useAuth.ts
+++ b/src/runtime/composables/local/useAuth.ts
@@ -4,6 +4,7 @@ import type { CommonUseAuthReturn, SignOutFunc, SignInFunc, GetSessionFunc, Seco
import { _fetch } from '../../utils/fetch'
import { jsonPointerGet, useTypedBackendConfig } from '../../helpers'
import { getRequestURLWN } from '../../utils/callWithNuxt'
+import { formatToken } from '../../utils/local'
import { useAuthState } from './useAuthState'
// @ts-expect-error - #auth not defined
import type { SessionData } from '#auth'
@@ -74,13 +75,18 @@ const getSession: GetSessionFunc = async (getSessionO
const config = useTypedBackendConfig(useRuntimeConfig(), 'local')
const { path, method } = config.endpoints.getSession
- const { data, loading, lastRefreshedAt, token, rawToken } = useAuthState()
+ const { data, loading, lastRefreshedAt, rawToken, token: tokenState, _internal } = useAuthState()
- if (!token.value && !getSessionOptions?.force) {
+ let token = tokenState.value
+ // For cached responses, return the token directly from the cookie
+ token ??= formatToken(_internal.rawTokenCookie.value)
+
+ if (!token && !getSessionOptions?.force) {
+ loading.value = false
return
}
- const headers = new Headers(token.value ? { [config.token.headerName]: token.value } as HeadersInit : undefined)
+ const headers = new Headers(token ? { [config.token.headerName]: token } as HeadersInit : undefined)
loading.value = true
try {
diff --git a/src/runtime/composables/local/useAuthState.ts b/src/runtime/composables/local/useAuthState.ts
index 38877bc8..1c41fd50 100644
--- a/src/runtime/composables/local/useAuthState.ts
+++ b/src/runtime/composables/local/useAuthState.ts
@@ -3,7 +3,8 @@ import type { CookieRef } from '#app'
import { type CommonUseAuthStateReturn } from '../../types'
import { makeCommonAuthState } from '../commonAuthState'
import { useTypedBackendConfig } from '../../helpers'
-import { useRuntimeConfig, useCookie, useState } from '#imports'
+import { formatToken } from '../../utils/local'
+import { useRuntimeConfig, useCookie, useState, onMounted } from '#imports'
// @ts-expect-error - #auth not defined
import type { SessionData } from '#auth'
@@ -12,6 +13,10 @@ interface UseAuthStateReturn extends CommonUseAuthStateReturn {
rawToken: CookieRef,
setToken: (newToken: string | null) => void
clearToken: () => void
+ _internal: {
+ baseURL: string,
+ rawTokenCookie: CookieRef
+ }
}
export const useAuthState = (): UseAuthStateReturn => {
@@ -24,12 +29,7 @@ export const useAuthState = (): UseAuthStateReturn => {
const rawToken = useState('auth:raw-token', () => _rawTokenCookie.value)
watch(rawToken, () => { _rawTokenCookie.value = rawToken.value })
- const token = computed(() => {
- if (rawToken.value === null) {
- return null
- }
- return config.token.type.length > 0 ? `${config.token.type} ${rawToken.value}` : rawToken.value
- })
+ const token = computed(() => formatToken(rawToken.value))
const setToken = (newToken: string | null) => {
rawToken.value = newToken
@@ -44,11 +44,22 @@ export const useAuthState = (): UseAuthStateReturn => {
rawToken
}
+ onMounted(() => {
+ // When the page is cached on a server, set the token on the client
+ if (_rawTokenCookie.value && !rawToken.value) {
+ setToken(_rawTokenCookie.value)
+ }
+ })
+
return {
...commonAuthState,
...schemeSpecificState,
setToken,
- clearToken
+ clearToken,
+ _internal: {
+ ...commonAuthState._internal,
+ rawTokenCookie: _rawTokenCookie
+ }
}
}
export default useAuthState
diff --git a/src/runtime/plugin.ts b/src/runtime/plugin.ts
index 9c27a8c9..819ab304 100644
--- a/src/runtime/plugin.ts
+++ b/src/runtime/plugin.ts
@@ -1,15 +1,18 @@
import { getHeader } from 'h3'
import authMiddleware from './middleware/auth'
+import { getNitroRouteRules } from './utils/kit'
import { addRouteMiddleware, defineNuxtPlugin, useRuntimeConfig, useAuth, useAuthState } from '#imports'
export default defineNuxtPlugin(async (nuxtApp) => {
// 1. Initialize authentication state, potentially fetch current session
- const { data, lastRefreshedAt } = useAuthState()
+ const { data, lastRefreshedAt, loading } = useAuthState()
const { getSession } = useAuth()
// use runtimeConfig
const runtimeConfig = useRuntimeConfig().public.auth
+ const routeRules = getNitroRouteRules(nuxtApp._route.path)
+
// Skip auth if we're prerendering
let nitroPrerender = false
if (nuxtApp.ssrContext) {
@@ -17,8 +20,17 @@ export default defineNuxtPlugin(async (nuxtApp) => {
getHeader(nuxtApp.ssrContext.event, 'x-nitro-prerender') !== undefined
}
+ // Prioritize `routeRules` setting over `runtimeConfig` settings, fallback to false
+ let disableServerSideAuth = routeRules.disableServerSideAuth
+ disableServerSideAuth ??= runtimeConfig?.disableServerSideAuth
+ disableServerSideAuth ??= false
+
+ if (disableServerSideAuth) {
+ loading.value = true
+ }
+
// Only fetch session if it was not yet initialized server-side
- if (typeof data.value === 'undefined' && !nitroPrerender) {
+ if (typeof data.value === 'undefined' && !nitroPrerender && !disableServerSideAuth) {
await getSession()
}
@@ -43,6 +55,10 @@ export default defineNuxtPlugin(async (nuxtApp) => {
let refreshTokenIntervalTimer: typeof refetchIntervalTimer
nuxtApp.hook('app:mounted', () => {
+ if (disableServerSideAuth) {
+ getSession()
+ }
+
document.addEventListener('visibilitychange', visibilityHandler, false)
if (enableRefreshPeriodically !== false) {
diff --git a/src/runtime/types.ts b/src/runtime/types.ts
index ce088b04..3b9d27f8 100644
--- a/src/runtime/types.ts
+++ b/src/runtime/types.ts
@@ -321,6 +321,21 @@ export interface ModuleOptions {
* Whether the module is enabled at all
*/
isEnabled?: boolean;
+ /**
+ * Forces your server to send a "loading" status on all requests, prompting the client to fetch on the client. If your website has caching, this prevents the server from caching someone's authentication status.
+ *
+ * This affects the entire site. For route-specific rules add `disableServerSideAuth` on `routeRules` instead:
+ ```ts
+ defineNuxtConfig({
+ routeRules: {
+ '/': { disableServerSideAuth: true }
+ }
+ })
+ ```
+ *
+ * @default false
+ */
+ disableServerSideAuth?: boolean;
/**
* Full url at which the app will run combined with the path to authentication. You can set this differently depending on your selected authentication-provider:
* - `authjs`: You must set the full URL, with origin and path in production. You can leave this empty in development
@@ -381,6 +396,15 @@ export interface ModuleOptions {
globalAppMiddleware?: GlobalMiddlewareOptions | boolean;
}
+export interface RouteOptions {
+ /**
+ * Forces your server to send a "loading" status on a route, prompting the client to fetch on the client. If a specific page has caching, this prevents the server from caching someone's authentication status.
+ *
+ * @default false
+ */
+ disableServerSideAuth: boolean;
+}
+
// Common useAuthStatus & useAuth return-types
export type SessionLastRefreshedAt = Date | undefined;
diff --git a/src/runtime/utils/kit.ts b/src/runtime/utils/kit.ts
new file mode 100644
index 00000000..2fe3de27
--- /dev/null
+++ b/src/runtime/utils/kit.ts
@@ -0,0 +1,45 @@
+import { withoutBase, withoutTrailingSlash } from 'ufo'
+import { createRouter, toRouteMatcher, type RouteMatcher } from 'radix3'
+import { type RouteOptions } from '../types'
+import { useRuntimeConfig } from '#imports'
+
+/**
+ * Removes query params from url path.
+ */
+export const withoutQuery = (path: string) => {
+ return path.split('?')[0]
+}
+
+let routeMatcher: RouteMatcher
+
+/**
+ * Creates a route matcher using the user's paths.
+ *
+ * In the returned function, enter a path to retrieve the routeRules that applies to that page.
+ */
+export const getNitroRouteRules = (path: string): Partial => {
+ const { nitro, app } = useRuntimeConfig()
+
+ if (!routeMatcher) {
+ routeMatcher = toRouteMatcher(
+ createRouter({
+ routes: Object.fromEntries(
+ Object.entries(nitro?.routeRules || {})
+ .map(([path, rules]) => [withoutTrailingSlash(path), rules])
+ )
+ })
+ )
+ }
+
+ const options: Partial = {}
+
+ const matches = routeMatcher.matchAll(
+ withoutBase(withoutTrailingSlash(withoutQuery(path)), app.baseURL)
+ ).reverse()
+
+ for (const match of matches) {
+ options.disableServerSideAuth ??= match.auth?.disableServerSideAuth
+ }
+
+ return options
+}
diff --git a/src/runtime/utils/local.ts b/src/runtime/utils/local.ts
new file mode 100644
index 00000000..062f021f
--- /dev/null
+++ b/src/runtime/utils/local.ts
@@ -0,0 +1,11 @@
+import { useTypedBackendConfig } from '../helpers'
+import { useRuntimeConfig } from '#imports'
+
+export const formatToken = (token: string | null) => {
+ const config = useTypedBackendConfig(useRuntimeConfig(), 'local')
+
+ if (token === null) {
+ return null
+ }
+ return config.token.type.length > 0 ? `${config.token.type} ${token}` : token
+}