feat: configure cors allowed headers (#6837)
## Description Currently, the Payload doesn't support to extend the Allowed Headers in CORS context. With this PR, `cors` property can be an object with `origins` and `headers`. - [x] I have read and understand the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository. ## Type of change - [ ] Chore (non-breaking change which does not add functionality) - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Change to the [templates](https://github.com/payloadcms/payload/tree/main/templates) directory (does not affect core functionality) - [ ] Change to the [examples](https://github.com/payloadcms/payload/tree/main/examples) directory (does not affect core functionality) - [x] This change requires a documentation update ## Checklist: - [x] I have added tests that prove my fix is effective or that my feature works - [x] Existing test suite passes locally with my changes - [x] I have made corresponding changes to the documentation Co-authored-by: Alessio Gravili <alessio@gravili.de>
This commit is contained in:
committed by
GitHub
parent
f494ebabbf
commit
9c72ab97b0
@@ -73,7 +73,7 @@ The following options are available:
|
|||||||
| **`serverURL`** | A string used to define the absolute URL of your app including the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port |
|
| **`serverURL`** | A string used to define the absolute URL of your app including the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port |
|
||||||
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
||||||
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
|
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
|
||||||
| **`cors`** | Either a whitelist array of URLS to allow CORS requests from, or a wildcard string (`'*'`) to accept incoming requests from any domain. |
|
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More](#cors) |
|
||||||
| **`localization`** | Opt-in and control how Payload handles the translation of your content into multiple locales. [More details](./localization). |
|
| **`localization`** | Opt-in and control how Payload handles the translation of your content into multiple locales. [More details](./localization). |
|
||||||
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
|
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
|
||||||
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
|
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
|
||||||
@@ -179,3 +179,37 @@ When `PAYLOAD_CONFIG_PATH` is set, Payload will use this path to load the config
|
|||||||
Payload collects **completely anonymous** telemetry data about general usage. This data is super important to us and helps us accurately understand how we're growing and what we can do to build the software into everything that it can possibly be. The telemetry that we collect also help us demonstrate our growth in an accurate manner, which helps us as we seek investment to build and scale our team. If we can accurately demonstrate our growth, we can more effectively continue to support Payload as free and open-source software. To opt out of telemetry, you can pass `telemetry: false` within your Payload Config.
|
Payload collects **completely anonymous** telemetry data about general usage. This data is super important to us and helps us accurately understand how we're growing and what we can do to build the software into everything that it can possibly be. The telemetry that we collect also help us demonstrate our growth in an accurate manner, which helps us as we seek investment to build and scale our team. If we can accurately demonstrate our growth, we can more effectively continue to support Payload as free and open-source software. To opt out of telemetry, you can pass `telemetry: false` within your Payload Config.
|
||||||
|
|
||||||
For more information about what we track, take a look at our [privacy policy](/privacy).
|
For more information about what we track, take a look at our [privacy policy](/privacy).
|
||||||
|
|
||||||
|
## Cross-origin resource sharing (CORS)
|
||||||
|
|
||||||
|
Cross-origin resource sharing (CORS) can be configured with either a whitelist array of URLS to allow CORS requests from, a wildcard string (`*`) to accept incoming requests from any domain, or a object with the following properties:
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `origins` | Either a whitelist array of URLS to allow CORS requests from, or a wildcard string (`'*'`) to accept incoming requests from any domain. |
|
||||||
|
| `headers | A list of allowed headers that will be appended in `Access-Control-Allow-Headers`. |
|
||||||
|
|
||||||
|
Here's an example showing how to allow incoming requests from any domain:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { buildConfig } from 'payload/config'
|
||||||
|
|
||||||
|
export default buildConfig({
|
||||||
|
// ...
|
||||||
|
cors: '*'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's an example showing how to append a new header (`x-custom-header`) in `Access-Control-Allow-Headers`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { buildConfig } from 'payload/config'
|
||||||
|
|
||||||
|
export default buildConfig({
|
||||||
|
// ...
|
||||||
|
cors: {
|
||||||
|
origins: ['http://localhost:3000']
|
||||||
|
headers: ['x-custom-header']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { PayloadRequest } from 'payload'
|
import type { CORSConfig, PayloadRequest } from 'payload'
|
||||||
|
|
||||||
type CorsArgs = {
|
type CorsArgs = {
|
||||||
headers: Headers
|
headers: Headers
|
||||||
@@ -9,15 +9,36 @@ export const headersWithCors = ({ headers, req }: CorsArgs): Headers => {
|
|||||||
const requestOrigin = req?.headers.get('Origin')
|
const requestOrigin = req?.headers.get('Origin')
|
||||||
|
|
||||||
if (cors) {
|
if (cors) {
|
||||||
headers.set('Access-Control-Allow-Methods', 'PUT, PATCH, POST, GET, DELETE, OPTIONS')
|
const defaultAllowedHeaders = [
|
||||||
headers.set(
|
'Origin',
|
||||||
'Access-Control-Allow-Headers',
|
'X-Requested-With',
|
||||||
'Origin, X-Requested-With, Content-Type, Accept, Authorization, Content-Encoding, x-apollo-tracing',
|
'Content-Type',
|
||||||
)
|
'Accept',
|
||||||
|
'Authorization',
|
||||||
|
'Content-Encoding',
|
||||||
|
'x-apollo-tracing',
|
||||||
|
]
|
||||||
|
|
||||||
if (cors === '*') {
|
headers.set('Access-Control-Allow-Methods', 'PUT, PATCH, POST, GET, DELETE, OPTIONS')
|
||||||
|
|
||||||
|
if (typeof cors === 'object' && 'headers' in cors) {
|
||||||
|
headers.set(
|
||||||
|
'Access-Control-Allow-Headers',
|
||||||
|
[...defaultAllowedHeaders, ...cors.headers].filter(Boolean).join(', '),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
headers.set('Access-Control-Allow-Headers', defaultAllowedHeaders.join(', '))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cors === '*' || (typeof cors === 'object' && 'origins' in cors && cors.origins === '*')) {
|
||||||
headers.set('Access-Control-Allow-Origin', '*')
|
headers.set('Access-Control-Allow-Origin', '*')
|
||||||
} else if (Array.isArray(cors) && cors.indexOf(requestOrigin) > -1) {
|
} else if (
|
||||||
|
(Array.isArray(cors) && cors.indexOf(requestOrigin) > -1) ||
|
||||||
|
(!Array.isArray(cors) &&
|
||||||
|
typeof cors === 'object' &&
|
||||||
|
'origins' in cors &&
|
||||||
|
cors.origins.indexOf(requestOrigin) > -1)
|
||||||
|
) {
|
||||||
headers.set('Access-Control-Allow-Credentials', 'true')
|
headers.set('Access-Control-Allow-Credentials', 'true')
|
||||||
headers.set('Access-Control-Allow-Origin', requestOrigin)
|
headers.set('Access-Control-Allow-Origin', requestOrigin)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,14 @@ export default joi.object({
|
|||||||
),
|
),
|
||||||
collections: joi.array(),
|
collections: joi.array(),
|
||||||
cookiePrefix: joi.string(),
|
cookiePrefix: joi.string(),
|
||||||
cors: [joi.string().valid('*'), joi.array().items(joi.string())],
|
cors: [
|
||||||
|
joi.string().valid('*'),
|
||||||
|
joi.array().items(joi.string()),
|
||||||
|
joi.object().keys({
|
||||||
|
headers: joi.array().items(joi.string()),
|
||||||
|
origins: [joi.string().valid('*'), joi.array().items(joi.string())],
|
||||||
|
}),
|
||||||
|
],
|
||||||
csrf: joi.array().items(joi.string().allow('')).sparse(),
|
csrf: joi.array().items(joi.string().allow('')).sparse(),
|
||||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||||
db: joi.any(),
|
db: joi.any(),
|
||||||
|
|||||||
@@ -440,6 +440,11 @@ export type SharpDependency = (
|
|||||||
options?: sharp.SharpOptions,
|
options?: sharp.SharpOptions,
|
||||||
) => sharp.Sharp
|
) => sharp.Sharp
|
||||||
|
|
||||||
|
export type CORSConfig = {
|
||||||
|
headers?: string[]
|
||||||
|
origins: '*' | string[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the central configuration
|
* This is the central configuration
|
||||||
*
|
*
|
||||||
@@ -592,7 +597,7 @@ export type Config = {
|
|||||||
*/
|
*/
|
||||||
cookiePrefix?: string
|
cookiePrefix?: string
|
||||||
/** Either a whitelist array of URLS to allow CORS requests from, or a wildcard string ('*') to accept incoming requests from any domain. */
|
/** Either a whitelist array of URLS to allow CORS requests from, or a wildcard string ('*') to accept incoming requests from any domain. */
|
||||||
cors?: '*' | string[]
|
cors?: '*' | CORSConfig | string[]
|
||||||
/** A whitelist array of URLs to allow Payload cookies to be accepted from as a form of CSRF protection. */
|
/** A whitelist array of URLs to allow Payload cookies to be accepted from as a form of CSRF protection. */
|
||||||
csrf?: string[]
|
csrf?: string[]
|
||||||
|
|
||||||
|
|||||||
@@ -113,4 +113,8 @@ export default buildConfigWithDefaults({
|
|||||||
typescript: {
|
typescript: {
|
||||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||||
},
|
},
|
||||||
|
cors: {
|
||||||
|
origins: '*',
|
||||||
|
headers: ['x-custom-header'],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import type { BlockField, Payload } from 'payload'
|
import type { BlockField, Payload } from 'payload'
|
||||||
|
|
||||||
|
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||||
|
|
||||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||||
import configPromise from './config.js'
|
import configPromise from './config.js'
|
||||||
|
|
||||||
|
let restClient: NextRESTClient
|
||||||
let payload: Payload
|
let payload: Payload
|
||||||
|
|
||||||
describe('Config', () => {
|
describe('Config', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
;({ payload } = await initPayloadInt(configPromise))
|
;({ payload, restClient } = await initPayloadInt(configPromise))
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -91,4 +94,11 @@ describe('Config', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('cors config', () => {
|
||||||
|
it('includes a custom header in Access-Control-Allow-Headers', async () => {
|
||||||
|
const response = await restClient.GET(`/pages`)
|
||||||
|
expect(response.headers.get('Access-Control-Allow-Headers')).toContain('x-custom-header')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user