Skip to content

Commit

Permalink
fix: prevent object merging with HeadersList
Browse files Browse the repository at this point in the history
  • Loading branch information
manchenkoff committed Oct 1, 2024
1 parent aff1d39 commit 393ddca
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 51 deletions.
31 changes: 15 additions & 16 deletions src/runtime/httpFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,32 @@ import type { SanctumAppConfig, SanctumInterceptor } from './types/config'
import type { ModuleOptions } from './types/options'
import { navigateTo, type NuxtApp } from '#app'

function configureClientInterceptors(
requestInterceptors: SanctumInterceptor[],
responseInterceptors: SanctumInterceptor[],
function useClientInterceptors(
options: ModuleOptions,
appConfig: SanctumAppConfig,
) {
): [SanctumInterceptor[], SanctumInterceptor[]] {
const [request, response] = [[] as SanctumInterceptor[], [] as SanctumInterceptor[]]

request.push(handleRequestHeaders)

if (options.mode === 'cookie') {
requestInterceptors.push(handleRequestCookies)
responseInterceptors.push(handleResponseHeaders)
request.push(handleRequestCookies)
response.push(handleResponseHeaders)
}

if (options.mode === 'token') {
requestInterceptors.push(handleRequestTokenHeader)
request.push(handleRequestTokenHeader)
}

if (appConfig.interceptors?.onRequest) {
requestInterceptors.push(appConfig.interceptors.onRequest)
request.push(appConfig.interceptors.onRequest)
}

if (appConfig.interceptors?.onResponse) {
responseInterceptors.push(appConfig.interceptors.onResponse)
response.push(appConfig.interceptors.onResponse)
}

return [request, response]
}

function determineCredentialsMode() {
Expand All @@ -51,15 +55,10 @@ export function createHttpClient(nuxtApp: NuxtApp, logger: ConsolaInstance): $Fe
const user = useSanctumUser()
const appConfig = useSanctumAppConfig()

const requestInterceptors: SanctumInterceptor[] = [handleRequestHeaders]
const responseInterceptors: SanctumInterceptor[] = []

configureClientInterceptors(
const [
requestInterceptors,
responseInterceptors,
options,
appConfig,
)
] = useClientInterceptors(options, appConfig)

const httpOptions: FetchOptions = {
baseURL: options.baseUrl,
Expand Down
17 changes: 13 additions & 4 deletions src/runtime/interceptors/common/request.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import type { FetchContext } from 'ofetch'
import type { ConsolaInstance } from 'consola'
import type { NuxtApp } from '#app'

/**
* Modify request before sending it to the Laravel API
* @param app Nuxt application instance
* @param ctx Fetch context
* @param logger Module logger instance
*/
export default async function handleRequestHeaders(
app: NuxtApp,
ctx: FetchContext,
) {
logger: ConsolaInstance,
): Promise<void> {
const method = ctx.options.method?.toLowerCase() ?? 'get'

ctx.options.headers = {
Accept: 'application/json',
...ctx.options.headers,
if (!ctx.options.headers) {
ctx.options.headers = {}
}

Object.assign(ctx.options.headers!, { Accept: 'application/json' })

// https://laravel.com/docs/10.x/routing#form-method-spoofing
if (method === 'put' && ctx.options.body instanceof FormData) {
ctx.options.method = 'POST'
ctx.options.body.append('_method', 'PUT')
}

logger.debug(
'[handleRequestHeaders] headers modified',
Object.keys(ctx.options.headers!),
)
}
55 changes: 28 additions & 27 deletions src/runtime/interceptors/cookie/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,30 @@ import {
type NuxtApp,
} from '#app'

type Headers = HeadersInit | undefined

const SECURE_METHODS = new Set(['post', 'delete', 'put', 'patch'])
const COOKIE_OPTIONS: { readonly: true } = { readonly: true }
const CLIENT_HEADERS = ['cookie', 'user-agent']

/**
* Pass all cookies, headers and referrer from the client to the API
* @param headers Headers collection to extend
* @param config Module configuration
* @returns {HeadersInit} Enriched headers collection
*/
function buildServerHeaders(
headers: Headers,
function appendServerHeaders(
headers: HeadersInit,
config: ModuleOptions,
): HeadersInit {
const clientHeaders = useRequestHeaders(CLIENT_HEADERS)
): void {
const clientHeaders = useRequestHeaders(['cookie', 'user-agent'])
const origin = config.origin ?? useRequestURL().origin

return {
...headers,
Referer: origin,
Origin: origin,
...clientHeaders,
}
Object.assign(
headers,
{
Referer: origin,
Origin: origin,
...(clientHeaders.cookie && { Cookie: clientHeaders.cookie }),
...(clientHeaders['user-agent'] && { 'User-Agent': clientHeaders['user-agent'] }),
},
)
}

/**
Expand All @@ -59,13 +58,12 @@ async function initCsrfCookie(
* @param headers Headers collection to extend
* @param config Module configuration
* @param logger Logger instance
* @returns {Promise<HeadersInit>} Enriched headers collection
*/
async function useCsrfHeader(
headers: Headers,
headers: HeadersInit,
config: ModuleOptions,
logger: ConsolaInstance,
): Promise<HeadersInit> {
): Promise<void> {
let csrfToken = useCookie(config.csrf.cookie!, COOKIE_OPTIONS)

if (!csrfToken.value) {
Expand All @@ -79,17 +77,15 @@ async function useCsrfHeader(
`${config.csrf.cookie} cookie is missing, unable to set ${config.csrf.header} header`,
)

return headers as HeadersInit
return
}

logger.debug(`Added ${config.csrf.header} header to pass to the API`)

return {
...headers,
...(csrfToken.value && {
[config.csrf.header!]: csrfToken.value,
}),
}
Object.assign(
headers,
{ [config.csrf.header!]: csrfToken.value },
)
}
/**
* Handle cookies and headers for the request
Expand All @@ -106,14 +102,19 @@ export default async function handleRequestCookies(
const method = ctx.options.method?.toLowerCase() ?? 'get'

if (import.meta.server) {
ctx.options.headers = buildServerHeaders(ctx.options.headers, config)
appendServerHeaders(ctx.options.headers!, config)
}

if (SECURE_METHODS.has(method)) {
ctx.options.headers = await useCsrfHeader(
ctx.options.headers,
await useCsrfHeader(
ctx.options.headers!,
config,
logger,
)
}

logger.debug(
'[handleRequestCookies] headers modified',
Object.keys(ctx.options.headers!),
)
}
10 changes: 6 additions & 4 deletions src/runtime/interceptors/token/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ export default async function handleRequestTokenHeader(
return
}

ctx.options.headers = {
...ctx.options.headers,
Authorization: `Bearer ${token}`,
}
Object.assign(ctx.options.headers!, { Authorization: `Bearer ${token}` })

logger.debug(
'[handleRequestTokenHeader] headers modified',
Object.keys(ctx.options.headers!),
)
}

0 comments on commit 393ddca

Please sign in to comment.