Skip to content

Commit

Permalink
chore: major refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
phoenix-ru committed Nov 7, 2024
1 parent 013fcc2 commit 1cd7381
Show file tree
Hide file tree
Showing 16 changed files with 180 additions and 147 deletions.
9 changes: 8 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
const topLevelDefaults = {
isEnabled: true,
baseURL: '/api/auth',
disableInternalRouting: false as boolean,
disableServerSideAuth: false,
originEnvKey: 'AUTH_ORIGIN',
sessionRefresh: {
Expand Down Expand Up @@ -126,7 +127,13 @@ export default defineNuxtModule<ModuleOptions>({

logger.info('`nuxt-auth` setup starting')

// 2. Set up runtime configuration
// 2.1. Disable internal routing for `local` provider when not specified otherwise
// https://github.com/sidebase/nuxt-auth/issues/797
if (userOptions.disableInternalRouting === undefined && selectedProvider === 'local') {
options.disableInternalRouting = true
}

// 2.2. Set up runtime configuration
if (!isProduction) {
const loggerMessages = [
`Selected provider: ${selectedProvider}.`,
Expand Down
21 changes: 12 additions & 9 deletions src/runtime/composables/authjs/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import type { AppProvider, BuiltInProviderType } from 'next-auth/providers/index
import { defu } from 'defu'
import { type Ref, readonly } from 'vue'
import { appendHeader } from 'h3'
import { determineCallbackUrl } from '../../utils/url'
import { getRequestURLWN, joinPathToApiURLWN, makeCWN, navigateToAuthPageWN } from '../../utils/callWithNuxt'
import { determineCallbackUrl, resolveApiUrlPath } from '../../utils/url'
import { _fetch } from '../../utils/fetch'
import { isNonEmptyObject } from '../../utils/checkSessionResult'
import type { CommonUseAuthReturn, GetSessionOptions, SignInFunc, SignOutFunc } from '../../types'
import { useTypedBackendConfig } from '../../helpers'
import { getRequestURLWN } from '../common/getRequestURL'
import type { SessionData } from './useAuthState'
import { navigateToAuthPageWN } from './utils/navigateToAuthPage'
import type { NuxtApp } from '#app/nuxt'
import { callWithNuxt } from '#app/nuxt'
import { createError, useAuthState, useNuxtApp, useRequestHeaders, useRuntimeConfig } from '#imports'
Expand Down Expand Up @@ -49,7 +50,9 @@ async function getCsrfToken() {
const headers = await getRequestCookies(nuxt)
return _fetch<{ csrfToken: string }>(nuxt, '/csrf', { headers }).then(response => response.csrfToken)
}
const getCsrfTokenWithNuxt = makeCWN(getCsrfToken)
function getCsrfTokenWithNuxt(nuxt: NuxtApp) {
return callWithNuxt(nuxt, getCsrfToken)
}

/**
* Trigger a sign in flow for the passed `provider`. If no provider is given the sign in page for all providers will be shown.
Expand All @@ -61,17 +64,16 @@ const getCsrfTokenWithNuxt = makeCWN(getCsrfToken)
type SignInResult = void | { error: string | null, status: number, ok: boolean, url: any }
const signIn: SignInFunc<SupportedProviders, SignInResult> = async (provider, options, authorizationParams) => {
const nuxt = useNuxtApp()
const runtimeConfig = await callWithNuxt(nuxt, useRuntimeConfig)

// 1. Lead to error page if no providers are available
const configuredProviders = await getProviders()
if (!configuredProviders) {
const errorUrl = await joinPathToApiURLWN(nuxt, 'error')
const errorUrl = resolveApiUrlPath('error', runtimeConfig)
return navigateToAuthPageWN(nuxt, errorUrl)
}

// 2. If no `provider` was given, either use the configured `defaultProvider` or `undefined` (leading to a forward to the `/login` page with all providers)
const runtimeConfig = await callWithNuxt(nuxt, useRuntimeConfig)

const backendConfig = useTypedBackendConfig(runtimeConfig, 'authjs')
if (typeof provider === 'undefined') {
// NOTE: `provider` might be an empty string
Expand All @@ -87,7 +89,7 @@ const signIn: SignInFunc<SupportedProviders, SignInResult> = async (provider, op
callbackUrl = await determineCallbackUrl(runtimeConfig.public.auth, () => getRequestURLWN(nuxt))
}

const signinUrl = await joinPathToApiURLWN(nuxt, 'signin')
const signinUrl = resolveApiUrlPath('signin', runtimeConfig)

const queryParams = callbackUrl ? `?${new URLSearchParams({ callbackUrl })}` : ''
const hrefSignInAllProviderPage = `${signinUrl}${queryParams}`
Expand Down Expand Up @@ -140,7 +142,6 @@ const signIn: SignInFunc<SupportedProviders, SignInResult> = async (provider, op

// At this point the request succeeded (i.e., it went through)
const error = new URL(data.url).searchParams.get('error')
// eslint-disable-next-line ts/no-use-before-define
await getSessionWithNuxt(nuxt)

return {
Expand Down Expand Up @@ -222,7 +223,9 @@ async function getSession(getSessionOptions?: GetSessionOptions): Promise<Sessio
headers
})
}
const getSessionWithNuxt = makeCWN(getSession)
function getSessionWithNuxt(nuxt: NuxtApp) {
return callWithNuxt(nuxt, getSession)
}

/**
* Sign out the current user.
Expand Down
53 changes: 53 additions & 0 deletions src/runtime/composables/authjs/utils/navigateToAuthPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { sanitizeStatusCode } from 'h3'
import { type NuxtApp, abortNavigation, callWithNuxt, useNuxtApp } from '#app'

export function navigateToAuthPageWN(nuxt: NuxtApp, href: string) {
return callWithNuxt(nuxt, navigateToAuthPage, [href])
}

/**
* Function to correctly navigate to auth-routes, necessary as the auth-routes are not part of the nuxt-app itself, so unknown to nuxt / vue-router.
*
* More specifically, we need this function to correctly handle the following cases:
* 1. On the client-side, returning `navigateTo(signInUrl)` leads to a `404` error as the next-auth-signin-page was not registered with the vue-router that is used for routing under the hood. For this reason we need to
* manually set `window.location.href` on the client **and then fake return a Promise that does not immediately resolve to block navigation (although it will not actually be fully awaited, but just be awaited long enough for the naviation to complete)**.
* 2. Additionally on the server-side, we cannot use `navigateTo(signInUrl)` as this uses `vue-router` internally which does not know the "external" sign-in page of next-auth and thus will log a warning which we want to avoid.
*
* Adapted from: https://github.com/nuxt/nuxt/blob/d188542a35bb541c7ed2e4502c687c2132979882/packages/nuxt/src/app/composables/router.ts#L161-L188
*
* @param href HREF / URL to navigate to
*/
export function navigateToAuthPage(href: string) {
const nuxtApp = useNuxtApp()

if (import.meta.server) {
if (nuxtApp.ssrContext) {
// TODO: consider deprecating in favour of `app:rendered` and removing
return nuxtApp.callHook('app:redirected').then(() => {
const encodedLoc = href.replace(/"/g, '%22')
const encodedHeader = new URL(href).toString()
nuxtApp.ssrContext!._renderResponse = {
statusCode: sanitizeStatusCode(302, 302),
body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`,
headers: { location: encodedHeader },
}
abortNavigation()
})
}
}

window.location.href = href
// If href contains a hash, the browser does not reload the page. We reload manually.
if (href.includes('#')) {
window.location.reload()
}

// TODO: Sadly, we cannot directly import types from `vue-router` as it leads to build failures. Typing the router about should help us to avoid manually typing `route` below
const router = nuxtApp.$router as { push: (href: string) => void }

// Wait for the `window.location.href` navigation from above to complete to avoid showing content. If that doesn't work fast enough, delegate navigation back to the `vue-router` (risking a vue-router 404 warning in the console, but still avoiding content-flashes of the protected target page)
const waitForNavigationWithFallbackToRouter = new Promise(resolve => setTimeout(resolve, 60 * 1000))
.then(() => router.push(href))

return waitForNavigationWithFallbackToRouter as Promise<void | undefined>
}
10 changes: 10 additions & 0 deletions src/runtime/composables/common/getRequestURL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import getURL from 'requrl'
import { type NuxtApp, callWithNuxt, useRequestEvent } from '#app'

export function getRequestURL(includePath = true) {
return getURL(useRequestEvent()?.node.req, includePath)
}

export function getRequestURLWN(nuxt: NuxtApp) {
return callWithNuxt(nuxt, getRequestURL)
}
6 changes: 0 additions & 6 deletions src/runtime/composables/commonAuthState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,10 @@ export function makeCommonAuthState<SessionData>() {
return 'unauthenticated'
})

const { origin, pathname } = useRuntimeConfig().public.auth.computed

return {
data,
loading,
lastRefreshedAt,
status,
_internal: {
origin,
pathname
}
}
}
2 changes: 1 addition & 1 deletion src/runtime/composables/local/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { type Ref, readonly } from 'vue'
import type { CommonUseAuthReturn, GetSessionOptions, SecondarySignInOptions, SignInFunc, SignOutFunc, SignUpOptions } from '../../types'
import { jsonPointerGet, objectFromJsonPointer, useTypedBackendConfig } from '../../helpers'
import { _fetch } from '../../utils/fetch'
import { getRequestURLWN } from '../../utils/callWithNuxt'
import { determineCallbackUrl } from '../../utils/url'
import { getRequestURLWN } from '../common/getRequestURL'
import { formatToken } from './utils/token'
import { type UseAuthStateReturn, useAuthState } from './useAuthState'
import { callWithNuxt } from '#app/nuxt'
Expand Down
3 changes: 0 additions & 3 deletions src/runtime/composables/local/useAuthState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ export interface UseAuthStateReturn extends CommonUseAuthStateReturn<SessionData
setToken: (newToken: string | null) => void
clearToken: () => void
_internal: {
origin?: string
pathname: string
rawTokenCookie: CookieRef<string | null>
}
}
Expand Down Expand Up @@ -108,7 +106,6 @@ export function useAuthState(): UseAuthStateReturn {
setToken,
clearToken,
_internal: {
...commonAuthState._internal,
rawTokenCookie: _rawTokenCookie
}
}
Expand Down
41 changes: 0 additions & 41 deletions src/runtime/composables/local/utils/url.ts

This file was deleted.

3 changes: 1 addition & 2 deletions src/runtime/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { navigateToAuthPages } from '../utils/url'
import { determineCallbackUrl } from '../utils/url'
import { isProduction } from '../helpers'
import { defineNuxtRouteMiddleware, navigateTo, useAuth, useRuntimeConfig } from '#imports'
Expand Down Expand Up @@ -91,7 +90,7 @@ export default defineNuxtRouteMiddleware((to) => {
const signInOptions: Parameters<typeof signIn>[1] = { error: 'SessionRequired', callbackUrl: determineCallbackUrl(authConfig, () => to.fullPath) }
// eslint-disable-next-line ts/ban-ts-comment
// @ts-ignore This is valid for a backend-type of `authjs`, where sign-in accepts a provider as a first argument
return signIn(undefined, signInOptions) as ReturnType<typeof navigateToAuthPages>
return signIn(undefined, signInOptions) as Promise<void>
}

// Redirect path was provided
Expand Down
8 changes: 4 additions & 4 deletions src/runtime/server/services/authjs/nuxtAuthHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ERROR_MESSAGES } from '../errors'
import { isNonEmptyObject } from '../../../utils/checkSessionResult'
import { getServerOrigin } from '../utils'
import { useTypedBackendConfig } from '../../../helpers'

import { resolveApiBaseURL } from '../../../utils/url'
import { useRuntimeConfig } from '#imports'

let preparedAuthjsHandler: ((req: RequestInternal) => Promise<ResponseInternal>) | undefined
Expand Down Expand Up @@ -102,15 +102,15 @@ export function NuxtAuthHandler(nuxtAuthOptions?: AuthOptions) {
/** Gets session on server-side */
export async function getServerSession(event: H3Event) {
const runtimeConfig = useRuntimeConfig()
const authBasePath = runtimeConfig.public.auth.computed.pathname
const authBasePathname = resolveApiBaseURL(runtimeConfig, true)
const trustHostUserPreference = useTypedBackendConfig(runtimeConfig, 'authjs').trustHost

// avoid running auth middleware on auth middleware (see #186)
if (event.path && event.path.startsWith(authBasePath)) {
if (event.path && event.path.startsWith(authBasePathname)) {
return null
}

const sessionUrlPath = joinURL(authBasePath, '/session')
const sessionUrlPath = joinURL(authBasePathname, '/session')
const headers = getHeaders(event) as HeadersInit
if (!preparedAuthjsHandler) {
// Edge-case: If no auth-endpoint was called yet, `preparedAuthHandler`-initialization was also not attempted as Nuxt lazily loads endpoints in production-mode.
Expand Down
22 changes: 10 additions & 12 deletions src/runtime/server/services/utils.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import type { H3Event } from 'h3'
import getURL from 'requrl'
import { parseURL } from 'ufo'
import { isProduction } from '../../helpers'
import { extractFromRuntimeConfig } from '../../utils/extractFromRuntimeConfig'
import { resolveApiBaseURL } from '../../utils/url'
import { ERROR_MESSAGES } from './errors'
import { useRuntimeConfig } from '#imports'

/**
* Get `origin` and fallback to `x-forwarded-host` or `host` headers if not in production.
*/
export function getServerOrigin(event?: H3Event): string {
const config = useRuntimeConfig()
const runtimeConfig = useRuntimeConfig()

// Prio 1: Environment variable
const envOriginKey = config.public.auth.originEnvKey
const envFromRuntimeConfig = extractFromRuntimeConfig(config, envOriginKey)
const envOrigin = envFromRuntimeConfig ?? process.env[envOriginKey]
if (envOrigin) {
return envOrigin
}
// Prio 2: Static configuration

// Prio 2: Computed origin
const runtimeConfigOrigin = config.public.auth.computed.origin
if (runtimeConfigOrigin) {
return runtimeConfigOrigin
// Resolve the value from runtime config/env.
// If the returned value has protocol and host, it is considered valid.
const baseURL = resolveApiBaseURL(runtimeConfig, false)
const parsed = parseURL(baseURL)
if (parsed.protocol && parsed.host) {
return `${parsed.protocol}//${parsed.host}`
}

// Prio 3: Try to infer the origin if we're not in production
Expand Down
16 changes: 16 additions & 0 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,21 @@ export interface ModuleOptions {
* Whether the module is enabled at all
*/
isEnabled?: boolean
/**
* Disables the Nuxt `$fetch` optimization. Do so when your auth logic is not handled by a Nuxt server (e.g. when using an external backend).
*
* Disabling the optimisation means that NuxtAuth will prefer calling `baseURL` + path instead of just path,
* which would often translate to an HTTP call.
*
* By default, this option is set to `false` for `authjs` provider.
* For `local` provider `disableInternalRouting` will default to `true` unless explicitly changed by user.
*
* ## Example
* With `disableInternalRouting: true` and `baseURL: 'https://example.com/api/auth'` your calls would be made to `https://example.com/api/auth` endpoints instead of `/api/auth`.
*
* @see https://nuxt.com/docs/api/utils/dollarfetch
*/
disableInternalRouting?: 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.
*
Expand Down Expand Up @@ -586,6 +601,7 @@ export type SignInFunc<PrimarySignInOptions, SignInResult> = (
export interface ModuleOptionsNormalized extends ModuleOptions {
isEnabled: boolean
baseURL: string
disableInternalRouting: boolean
// Cannot use `DeepRequired` here because it leads to build issues
provider: Required<NonNullable<ModuleOptions['provider']>>
sessionRefresh: NonNullable<ModuleOptions['sessionRefresh']>
Expand Down
9 changes: 0 additions & 9 deletions src/runtime/utils/callWithNuxt.ts

This file was deleted.

17 changes: 10 additions & 7 deletions src/runtime/utils/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { joinPathToApiURL } from './url'
import { callWithNuxt } from '#app/nuxt'
import { resolveApiUrlPath } from './url'
import { callWithNuxt, useRuntimeConfig } from '#app'
import type { useNuxtApp } from '#imports'

export async function _fetch<T>(nuxt: ReturnType<typeof useNuxtApp>, path: string, fetchOptions?: Parameters<typeof $fetch>[1]): Promise<T> {
const runtimeConfig = await callWithNuxt(nuxt, useRuntimeConfig)
const joinedPath = resolveApiUrlPath(path, runtimeConfig)
try {
const joinedPath = await callWithNuxt(nuxt, () => joinPathToApiURL(path))
return $fetch(joinedPath, fetchOptions)
}
catch (error) {
// TODO: Adapt this error to be more generic
console.error(
'Error in `nuxt-auth`-app-side data fetching: Have you added the authentication handler server-endpoint `[...].ts`? Have you added the authentication handler in a non-default location (default is `~/server/api/auth/[...].ts`) and not updated the module-setting `auth.basePath`? Error is:'
)
let errorMessage = `[@sidebase/nuxt-auth] Error while requesting ${joinedPath}.`
if (runtimeConfig.public.auth.provider.type === 'authjs') {
errorMessage += ' Have you added the authentication handler server-endpoint `[...].ts`? Have you added the authentication handler in a non-default location (default is `~/server/api/auth/[...].ts`) and not updated the module-setting `auth.basePath`?'
}
errorMessage += ' Error is:'
console.error(errorMessage)
console.error(error)

throw new Error(
Expand Down
Loading

0 comments on commit 1cd7381

Please sign in to comment.