Compare commits
9 Commits
v3.0.0-bet
...
fix/lint-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b96e1776f | ||
|
|
014ee1a1b2 | ||
|
|
cf6da0186b | ||
|
|
18063bd256 | ||
|
|
76b3075369 | ||
|
|
3d63ce94bb | ||
|
|
f8a5103ed7 | ||
|
|
2bd53a06eb | ||
|
|
442518dbc9 |
@@ -599,7 +599,7 @@ export const Orders: CollectionConfig = {
|
||||
{
|
||||
path: '/:id/tracking',
|
||||
method: 'get',
|
||||
handler: (req) => {
|
||||
handler: async (req) => {
|
||||
const tracking = await getTrackingInfo(req.params.id)
|
||||
|
||||
if (!tracking) {
|
||||
@@ -614,7 +614,7 @@ export const Orders: CollectionConfig = {
|
||||
{
|
||||
path: '/:id/tracking',
|
||||
method: 'post',
|
||||
handler: (req) => {
|
||||
handler: async (req) => {
|
||||
// `data` is not automatically appended to the request
|
||||
// if you would like to read the body of the request
|
||||
// you can use `data = await req.json()`
|
||||
@@ -654,7 +654,7 @@ import { addDataAndFileToRequest } from '@payloadcms/next/utilities'
|
||||
{
|
||||
path: '/:id/tracking',
|
||||
method: 'post',
|
||||
handler: (req) => {
|
||||
handler: async (req) => {
|
||||
await addDataAndFileToRequest(req)
|
||||
await req.payload.update({
|
||||
collection: 'tracking',
|
||||
@@ -680,7 +680,7 @@ import { addLocalesToRequestFromData } from '@payloadcms/next/utilities'
|
||||
{
|
||||
path: '/:id/tracking',
|
||||
method: 'post',
|
||||
handler: (req) => {
|
||||
handler: async (req) => {
|
||||
await addLocalesToRequestFromData(req)
|
||||
// you now can access req.locale & req.fallbackLocale
|
||||
return Response.json({ message: 'success' })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Payload Multi-Tenant Example
|
||||
# Payload Multi-Tenant Example (Single Domain)
|
||||
|
||||
This example demonstrates how to achieve a multi-tenancy in [Payload](https://github.com/payloadcms/payload) on a single domain. Tenants are separated by a `Tenants` collection.
|
||||
|
||||
@@ -6,12 +6,16 @@ This example demonstrates how to achieve a multi-tenancy in [Payload](https://gi
|
||||
|
||||
To spin up this example locally, follow these steps:
|
||||
|
||||
1. First clone the repo
|
||||
2. `cd YOUR_PROJECT_REPO && cp .env.example .env`
|
||||
3. `pnpm i && pnpm dev`
|
||||
4. run `yarn seed` to seed the database
|
||||
5. open `http://localhost:3000/admin` to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
1. Clone this repo
|
||||
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
|
||||
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
- Press `y` when prompted to seed the database
|
||||
1. `open http://localhost:3000` to access the home page
|
||||
1. `open http://localhost:3000/admin` to access the admin panel
|
||||
- Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
## How it works
|
||||
|
||||
|
||||
8
examples/multi-tenant-single-domain/next.config.mjs
Normal file
8
examples/multi-tenant-single-domain/next.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
import { withPayload } from '@payloadcms/next/withPayload'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Your Next.js config here
|
||||
}
|
||||
|
||||
export default withPayload(nextConfig)
|
||||
@@ -1,21 +1,18 @@
|
||||
{
|
||||
"name": "multi-tenant-single-domain",
|
||||
"description": "An example of a multi tenant application, using a single domain",
|
||||
"version": "1.0.0",
|
||||
"description": "An example of a multi tenant application, using a single domain",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --turbo",
|
||||
"_dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
|
||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
|
||||
"generate:types": "payload generate:types",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation && pnpm seed && next dev --turbo",
|
||||
"generate:schema": "payload-graphql generate:schema",
|
||||
"seed": "tsx ./scripts/seed.ts"
|
||||
"generate:types": "payload generate:types",
|
||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||
"seed": "npm run payload migrate:fresh",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/db-mongodb": "3.0.0-beta.58",
|
||||
@@ -42,6 +39,9 @@
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "5.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-beta.2",
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
/**
|
||||
* This is an example of a standalone script that loads in the Payload config
|
||||
* and uses the Payload Local API to query the database.
|
||||
*/
|
||||
|
||||
process.env.PAYLOAD_DROP_DATABASE = 'true'
|
||||
|
||||
import type { Payload, RequiredDataFromCollectionSlug } from 'payload'
|
||||
|
||||
import { getPayload } from 'payload'
|
||||
import { importConfig } from 'payload/node'
|
||||
|
||||
async function findOrCreateTenant({ data, payload }: { data: any; payload: Payload }) {
|
||||
const tenantsQuery = await payload.find({
|
||||
collection: 'tenants',
|
||||
where: {
|
||||
slug: {
|
||||
equals: data.slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (tenantsQuery.docs?.[0]) return tenantsQuery.docs[0]
|
||||
|
||||
return payload.create({
|
||||
collection: 'tenants',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
async function findOrCreateUser({
|
||||
data,
|
||||
payload,
|
||||
}: {
|
||||
data: RequiredDataFromCollectionSlug<'users'>
|
||||
payload: Payload
|
||||
}) {
|
||||
const usersQuery = await payload.find({
|
||||
collection: 'users',
|
||||
where: {
|
||||
email: {
|
||||
equals: data.email,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (usersQuery.docs?.[0]) return usersQuery.docs[0]
|
||||
|
||||
return payload.create({
|
||||
collection: 'users',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
async function findOrCreatePage({ data, payload }: { data: any; payload: Payload }) {
|
||||
const pagesQuery = await payload.find({
|
||||
collection: 'pages',
|
||||
where: {
|
||||
slug: {
|
||||
equals: data.slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (pagesQuery.docs?.[0]) return pagesQuery.docs[0]
|
||||
|
||||
return payload.create({
|
||||
collection: 'pages',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const awaitedConfig = await importConfig('../src/payload.config.ts')
|
||||
const payload = await getPayload({ config: awaitedConfig })
|
||||
|
||||
const tenant1 = await findOrCreateTenant({
|
||||
data: {
|
||||
name: 'Tenant 1',
|
||||
slug: 'tenant-1',
|
||||
},
|
||||
payload,
|
||||
})
|
||||
|
||||
const tenant2 = await findOrCreateTenant({
|
||||
data: {
|
||||
name: 'Tenant 2',
|
||||
slug: 'tenant-2',
|
||||
public: true,
|
||||
},
|
||||
payload,
|
||||
})
|
||||
|
||||
const tenant3 = await findOrCreateTenant({
|
||||
data: {
|
||||
name: 'Tenant 3',
|
||||
slug: 'tenant-3',
|
||||
},
|
||||
payload,
|
||||
})
|
||||
|
||||
await findOrCreateUser({
|
||||
data: {
|
||||
email: 'demo@payloadcms.com',
|
||||
password: 'demo',
|
||||
roles: ['super-admin'],
|
||||
},
|
||||
payload,
|
||||
})
|
||||
|
||||
await findOrCreateUser({
|
||||
data: {
|
||||
email: 'tenant1@payloadcms.com',
|
||||
password: 'test',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant1.id,
|
||||
},
|
||||
],
|
||||
username: 'tenant1',
|
||||
},
|
||||
payload,
|
||||
})
|
||||
|
||||
await findOrCreateUser({
|
||||
data: {
|
||||
email: 'tenant2@payloadcms.com',
|
||||
password: 'test',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant2.id,
|
||||
},
|
||||
],
|
||||
username: 'tenant2',
|
||||
},
|
||||
payload,
|
||||
})
|
||||
|
||||
await findOrCreateUser({
|
||||
data: {
|
||||
email: 'tenant3@payloadcms.com',
|
||||
password: 'test',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant3.id,
|
||||
},
|
||||
],
|
||||
username: 'tenant3',
|
||||
},
|
||||
payload,
|
||||
})
|
||||
|
||||
await findOrCreateUser({
|
||||
data: {
|
||||
email: 'multi-admin@payloadcms.com',
|
||||
password: 'test',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant1.id,
|
||||
},
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant2.id,
|
||||
},
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant3.id,
|
||||
},
|
||||
],
|
||||
username: 'tenant3',
|
||||
},
|
||||
payload,
|
||||
})
|
||||
|
||||
await findOrCreatePage({
|
||||
data: {
|
||||
slug: 'home',
|
||||
tenant: tenant1.id,
|
||||
title: 'Page for Tenant 1',
|
||||
},
|
||||
payload,
|
||||
})
|
||||
|
||||
await findOrCreatePage({
|
||||
data: {
|
||||
slug: 'home',
|
||||
tenant: tenant2.id,
|
||||
title: 'Page for Tenant 2',
|
||||
},
|
||||
payload,
|
||||
})
|
||||
|
||||
await findOrCreatePage({
|
||||
data: {
|
||||
slug: 'home',
|
||||
tenant: tenant3.id,
|
||||
title: 'Page for Tenant 3',
|
||||
},
|
||||
payload,
|
||||
})
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
run().catch(console.error)
|
||||
@@ -2,7 +2,7 @@ import type { Where } from 'payload'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { headers as getHeaders } from 'next/headers'
|
||||
import { notFound, redirect } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export default {};
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { PayloadRequest } from "payload";
|
||||
|
||||
export const isPayloadAdminPanel = (req: PayloadRequest) => {
|
||||
return req.headers.has('referer') && req.headers.get('referer')?.startsWith(`${process.env.NEXT_PUBLIC_SERVER_URL}${req.payload.config.routes.admin}`)
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import type { Access } from 'payload'
|
||||
|
||||
import { parseCookies } from 'payload'
|
||||
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
export const filterByTenantRead: Access = (args) => {
|
||||
const req = args.req
|
||||
@@ -2,7 +2,7 @@ import type { FieldHook } from 'payload'
|
||||
|
||||
import { ValidationError } from 'payload'
|
||||
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
export const ensureUniqueSlug: FieldHook = async ({ data, originalDoc, req, value }) => {
|
||||
// if value is unchanged, skip validation
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { tenantField } from '../../fields/TenantField/index.js'
|
||||
import { isPayloadAdminPanel } from '../../utilities/isPayloadAdminPanel.js'
|
||||
import { canMutatePage, filterByTenantRead } from './access/byTenant.js'
|
||||
import { externalReadAccess } from './access/externalReadAccess.js'
|
||||
import { ensureUniqueSlug } from './hooks/ensureUniqueSlug.js'
|
||||
import { tenantField } from '../../fields/TenantField'
|
||||
import { isPayloadAdminPanel } from '../../utilities/isPayloadAdminPanel'
|
||||
import { canMutatePage, filterByTenantRead } from './access/byTenant'
|
||||
import { externalReadAccess } from './access/externalReadAccess'
|
||||
import { ensureUniqueSlug } from './hooks/ensureUniqueSlug'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
@@ -2,8 +2,8 @@ import type { Access } from 'payload'
|
||||
|
||||
import { parseCookies } from 'payload'
|
||||
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
export const filterByTenantRead: Access = (args) => {
|
||||
const req = args.req
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Access } from 'payload'
|
||||
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
export const tenantRead: Access = (args) => {
|
||||
const req = args.req
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { isSuperAdmin } from '../../access/isSuperAdmin.js'
|
||||
import { canMutateTenant, filterByTenantRead } from './access/byTenant.js'
|
||||
import { isSuperAdmin } from '../../access/isSuperAdmin'
|
||||
import { canMutateTenant, filterByTenantRead } from './access/byTenant'
|
||||
|
||||
export const Tenants: CollectionConfig = {
|
||||
slug: 'tenants',
|
||||
@@ -2,8 +2,8 @@ import type { Access } from 'payload'
|
||||
|
||||
import type { User } from '../../../../payload-types'
|
||||
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
|
||||
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
export const createAccess: Access<User> = (args) => {
|
||||
const { req } = args
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Access } from 'payload'
|
||||
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
|
||||
import { isAccessingSelf } from './isAccessingSelf.js'
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||
import { isAccessingSelf } from './isAccessingSelf'
|
||||
|
||||
export const isSuperAdminOrSelf: Access = (args) => isSuperAdmin(args) || isAccessingSelf(args)
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { User } from '@/payload-types'
|
||||
import type { Access, Where } from 'payload'
|
||||
|
||||
import { isSuperAdmin } from '@/cms/access/isSuperAdmin'
|
||||
import { getTenantAdminTenantAccessIDs } from '@/cms/utilities/getTenantAccessIDs'
|
||||
import { parseCookies } from 'payload'
|
||||
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
export const readAccess: Access<User> = (args) => {
|
||||
const { req } = args
|
||||
if (!req?.user) return false
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Access } from 'payload'
|
||||
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
|
||||
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
export const updateAndDeleteAccess: Access = (args) => {
|
||||
const { req } = args
|
||||
@@ -2,7 +2,7 @@ import type { FieldHook } from 'payload'
|
||||
|
||||
import { ValidationError } from 'payload'
|
||||
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
export const ensureUniqueUsername: FieldHook = async ({ data, originalDoc, req, value }) => {
|
||||
// if value is unchanged, skip validation
|
||||
@@ -2,12 +2,12 @@ import type { CollectionConfig } from 'payload'
|
||||
|
||||
import type { User } from '../../../payload-types'
|
||||
|
||||
import { getTenantAdminTenantAccessIDs } from '../../utilities/getTenantAccessIDs.js'
|
||||
import { createAccess } from './access/create.js'
|
||||
import { getTenantAdminTenantAccessIDs } from '../../utilities/getTenantAccessIDs'
|
||||
import { createAccess } from './access/create'
|
||||
import { readAccess } from './access/read'
|
||||
import { updateAndDeleteAccess } from './access/updateAndDelete.js'
|
||||
import { externalUsersLogin } from './endpoints/externalUsersLogin.js'
|
||||
import { ensureUniqueUsername } from './hooks/ensureUniqueUsername.js'
|
||||
import { updateAndDeleteAccess } from './access/updateAndDelete'
|
||||
import { externalUsersLogin } from './endpoints/externalUsersLogin'
|
||||
import { ensureUniqueUsername } from './hooks/ensureUniqueUsername'
|
||||
|
||||
const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cookies as getCookies } from 'next/headers'
|
||||
import React from 'react'
|
||||
|
||||
import { TenantSelector } from './index.client.js'
|
||||
import { TenantSelector } from './index.client'
|
||||
|
||||
export const TenantSelectorRSC = () => {
|
||||
const cookies = getCookies()
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FieldAccess } from 'payload'
|
||||
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin.js'
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
export const tenantFieldUpdate: FieldAccess = (args) => {
|
||||
const tenantIDs = getTenantAccessIDs(args.req.user)
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FieldHook } from 'payload'
|
||||
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs.js'
|
||||
import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
export const autofillTenant: FieldHook = ({ req, value }) => {
|
||||
// If there is no value,
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { isSuperAdmin } from '../../access/isSuperAdmin.js'
|
||||
import { tenantFieldUpdate } from './access/update.js'
|
||||
import { TenantFieldComponent } from './components/Field.js'
|
||||
import { autofillTenant } from './hooks/autofillTenant.js'
|
||||
import { isSuperAdmin } from '../../access/isSuperAdmin'
|
||||
import { tenantFieldUpdate } from './access/update'
|
||||
import { TenantFieldComponent } from './components/Field'
|
||||
import { autofillTenant } from './hooks/autofillTenant'
|
||||
|
||||
export const tenantField: Field = {
|
||||
name: 'tenant',
|
||||
131
examples/multi-tenant-single-domain/src/migrations/seed.ts
Normal file
131
examples/multi-tenant-single-domain/src/migrations/seed.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
|
||||
|
||||
export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'demo@payloadcms.com',
|
||||
password: 'demo',
|
||||
roles: ['super-admin'],
|
||||
},
|
||||
})
|
||||
|
||||
const tenant1 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 1',
|
||||
slug: 'tenant-1',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant2 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 2',
|
||||
slug: 'tenant-2',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant3 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 3',
|
||||
slug: 'tenant-3',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant1@payloadcms.com',
|
||||
password: 'test',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant1.id,
|
||||
},
|
||||
],
|
||||
username: 'tenant1',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant2@payloadcms.com',
|
||||
password: 'test',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant2.id,
|
||||
},
|
||||
],
|
||||
username: 'tenant2',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant3@payloadcms.com',
|
||||
password: 'test',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant3.id,
|
||||
},
|
||||
],
|
||||
username: 'tenant3',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'multi-admin@payloadcms.com',
|
||||
password: 'test',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant1.id,
|
||||
},
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant2.id,
|
||||
},
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant3.id,
|
||||
},
|
||||
],
|
||||
username: 'tenant3',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
slug: 'home',
|
||||
tenant: tenant1.id,
|
||||
title: 'Page for Tenant 1',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
slug: 'home',
|
||||
tenant: tenant2.id,
|
||||
title: 'Page for Tenant 2',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
slug: 'home',
|
||||
tenant: tenant3.id,
|
||||
title: 'Page for Tenant 3',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
@@ -131,4 +131,4 @@ export interface Auth {
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import path from 'path'
|
||||
import { buildConfig } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { Pages } from './cms/collections/Pages/index.js'
|
||||
import { Tenants } from './cms/collections/Tenants/index.js'
|
||||
import Users from './cms/collections/Users/index.js'
|
||||
import { TenantSelectorRSC } from './cms/components/TenantSelector/index.js'
|
||||
import { Pages } from './collections/Pages'
|
||||
import { Tenants } from './collections/Tenants'
|
||||
import Users from './collections/Users'
|
||||
import { TenantSelectorRSC } from './components/TenantSelector'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
export const isPayloadAdminPanel = (req: PayloadRequest) => {
|
||||
return (
|
||||
req.headers.has('referer') &&
|
||||
req.headers
|
||||
.get('referer')
|
||||
?.startsWith(`${process.env.NEXT_PUBLIC_SERVER_URL}${req.payload.config.routes.admin}`)
|
||||
)
|
||||
}
|
||||
@@ -27,10 +27,10 @@
|
||||
"./src/*"
|
||||
],
|
||||
"@payload-config": [
|
||||
"./src/payload.config.ts"
|
||||
"src/payload.config.ts"
|
||||
],
|
||||
"@payload-types": [
|
||||
"./src/payload-types.ts"
|
||||
"src/payload-types.ts"
|
||||
]
|
||||
},
|
||||
"target": "ES2017"
|
||||
|
||||
@@ -47,6 +47,16 @@
|
||||
"test": "jest",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@sindresorhus/slugify": "^1.1.0",
|
||||
|
||||
@@ -32,6 +32,16 @@
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bson-objectid": "2.0.4",
|
||||
"deepmerge": "4.3.1",
|
||||
|
||||
@@ -43,6 +43,16 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"renamePredefinedMigrations": "tsx ./scripts/renamePredefinedMigrations.ts"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@libsql/client": "^0.5.2",
|
||||
"console-table-printer": "2.11.2",
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"nodemailer": "6.9.10"
|
||||
},
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"test": "jest"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.12",
|
||||
"jest": "^29.7.0",
|
||||
|
||||
@@ -16,6 +16,16 @@
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint-react/eslint-plugin": "1.5.25-next.4",
|
||||
"@eslint/js": "9.6.0",
|
||||
|
||||
@@ -16,6 +16,16 @@
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint-react/eslint-plugin": "1.5.25-next.4",
|
||||
"@eslint/js": "9.6.0",
|
||||
|
||||
@@ -40,6 +40,16 @@
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"graphql-scalars": "1.22.2",
|
||||
"pluralize": "8.0.0",
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/live-preview": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/live-preview": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"payload": "workspace:*"
|
||||
|
||||
@@ -61,6 +61,16 @@
|
||||
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "6.0.8",
|
||||
"@payloadcms/graphql": "workspace:*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Collection, PayloadRequest, SanitizedConfig } from 'payload'
|
||||
|
||||
import httpStatus from 'http-status'
|
||||
import { APIError } from 'payload'
|
||||
import { APIError, APIErrorName, ValidationErrorName } from 'payload'
|
||||
|
||||
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
||||
import { headersWithCors } from '../../utilities/headersWithCors.js'
|
||||
@@ -16,7 +16,7 @@ const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorRes
|
||||
|
||||
// Payload 'ValidationError' and 'APIError'
|
||||
if (
|
||||
(proto.constructor.name === 'ValidationError' || proto.constructor.name === 'APIError') &&
|
||||
(proto.constructor.name === ValidationErrorName || proto.constructor.name === APIErrorName) &&
|
||||
incoming.data
|
||||
) {
|
||||
return {
|
||||
@@ -31,7 +31,7 @@ const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorRes
|
||||
}
|
||||
|
||||
// Mongoose 'ValidationError': https://mongoosejs.com/docs/api/error.html#Error.ValidationError
|
||||
if (proto.constructor.name === 'ValidationError' && 'errors' in incoming && incoming.errors) {
|
||||
if (proto.constructor.name === ValidationErrorName && 'errors' in incoming && incoming.errors) {
|
||||
return {
|
||||
errors: Object.keys(incoming.errors).reduce((acc, key) => {
|
||||
acc.push({
|
||||
|
||||
@@ -83,6 +83,16 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"pretest": "pnpm build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/env": "^15.0.0-rc.0",
|
||||
"@payloadcms/translations": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import type { Config } from '../../config/types.js'
|
||||
import type { CollectionConfig, Field } from '../../index.js'
|
||||
|
||||
import { ReservedFieldName } from '../../errors/index.js'
|
||||
import { sanitizeCollection } from './sanitize.js'
|
||||
|
||||
describe('reservedFieldNames - collections -', () => {
|
||||
const config = {
|
||||
collections: [],
|
||||
globals: [],
|
||||
} as Partial<Config>
|
||||
|
||||
describe('uploads -', () => {
|
||||
const collectionWithUploads: CollectionConfig = {
|
||||
slug: 'collection-with-uploads',
|
||||
fields: [],
|
||||
upload: true,
|
||||
}
|
||||
|
||||
it('should throw on file', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'file',
|
||||
type: 'text',
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [
|
||||
{
|
||||
...collectionWithUploads,
|
||||
fields,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...collectionWithUploads,
|
||||
fields,
|
||||
},
|
||||
)
|
||||
}).rejects.toThrow(ReservedFieldName)
|
||||
})
|
||||
|
||||
it('should not throw on a custom field', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'customField',
|
||||
type: 'text',
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [
|
||||
{
|
||||
...collectionWithUploads,
|
||||
fields,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...collectionWithUploads,
|
||||
fields,
|
||||
},
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('auth -', () => {
|
||||
const collectionWithAuth: CollectionConfig = {
|
||||
slug: 'collection-with-auth',
|
||||
fields: [],
|
||||
auth: {
|
||||
verify: true,
|
||||
useAPIKey: true,
|
||||
loginWithUsername: true,
|
||||
},
|
||||
}
|
||||
|
||||
it('should throw on hash', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'hash',
|
||||
type: 'text',
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [
|
||||
{
|
||||
...collectionWithAuth,
|
||||
fields,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...collectionWithAuth,
|
||||
fields,
|
||||
},
|
||||
)
|
||||
}).rejects.toThrow(ReservedFieldName)
|
||||
})
|
||||
|
||||
it('should throw on salt', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'salt',
|
||||
type: 'text',
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [
|
||||
{
|
||||
...collectionWithAuth,
|
||||
fields,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...collectionWithAuth,
|
||||
fields,
|
||||
},
|
||||
)
|
||||
}).rejects.toThrow(ReservedFieldName)
|
||||
})
|
||||
|
||||
it('should not throw on a custom field', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'customField',
|
||||
type: 'text',
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [
|
||||
{
|
||||
...collectionWithAuth,
|
||||
fields,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...collectionWithAuth,
|
||||
fields,
|
||||
},
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
150
packages/payload/src/collections/config/reservedFieldNames.ts
Normal file
150
packages/payload/src/collections/config/reservedFieldNames.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { CollectionConfig } from '../../index.js'
|
||||
|
||||
import { ReservedFieldName } from '../../errors/ReservedFieldName.js'
|
||||
import { fieldAffectsData } from '../../fields/config/types.js'
|
||||
|
||||
// Note for future reference: We've slimmed down the reserved field names but left them in here for reference in case it's needed in the future.
|
||||
|
||||
/**
|
||||
* Reserved field names for collections with auth config enabled
|
||||
*/
|
||||
const reservedBaseAuthFieldNames = [
|
||||
/* 'email',
|
||||
'resetPasswordToken',
|
||||
'resetPasswordExpiration', */
|
||||
'salt',
|
||||
'hash',
|
||||
]
|
||||
/**
|
||||
* Reserved field names for auth collections with verify: true
|
||||
*/
|
||||
const reservedVerifyFieldNames = [
|
||||
/* '_verified', '_verificationToken' */
|
||||
]
|
||||
/**
|
||||
* Reserved field names for auth collections with useApiKey: true
|
||||
*/
|
||||
const reservedAPIKeyFieldNames = [
|
||||
/* 'enableAPIKey', 'apiKeyIndex', 'apiKey' */
|
||||
]
|
||||
|
||||
/**
|
||||
* Reserved field names for collections with upload config enabled
|
||||
*/
|
||||
const reservedBaseUploadFieldNames = [
|
||||
'file',
|
||||
/* 'mimeType',
|
||||
'thumbnailURL',
|
||||
'width',
|
||||
'height',
|
||||
'filesize',
|
||||
'filename',
|
||||
'url',
|
||||
'focalX',
|
||||
'focalY',
|
||||
'sizes', */
|
||||
]
|
||||
|
||||
/**
|
||||
* Reserved field names for collections with versions enabled
|
||||
*/
|
||||
const reservedVersionsFieldNames = [
|
||||
/* '__v', '_status' */
|
||||
]
|
||||
|
||||
/**
|
||||
* Sanitize fields for collections with auth config enabled.
|
||||
*
|
||||
* Should run on top level fields only.
|
||||
*/
|
||||
export const sanitizeAuthFields = (fields: Field[], config: CollectionConfig) => {
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i]
|
||||
|
||||
if (fieldAffectsData(field) && field.name) {
|
||||
if (config.auth && typeof config.auth === 'object' && !config.auth.disableLocalStrategy) {
|
||||
const auth = config.auth
|
||||
|
||||
if (reservedBaseAuthFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
|
||||
if (auth.verify) {
|
||||
if (reservedAPIKeyFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
}
|
||||
|
||||
/* if (auth.maxLoginAttempts) {
|
||||
if (field.name === 'loginAttempts' || field.name === 'lockUntil') {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
} */
|
||||
|
||||
/* if (auth.loginWithUsername) {
|
||||
if (field.name === 'username') {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
} */
|
||||
|
||||
if (auth.verify) {
|
||||
if (reservedVerifyFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tabs without a name
|
||||
if (field.type === 'tabs') {
|
||||
for (let j = 0; j < field.tabs.length; j++) {
|
||||
const tab = field.tabs[j]
|
||||
|
||||
if (!('name' in tab)) {
|
||||
sanitizeAuthFields(tab.fields, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle presentational fields like rows and collapsibles
|
||||
if (!fieldAffectsData(field) && 'fields' in field && field.fields) {
|
||||
sanitizeAuthFields(field.fields, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize fields for collections with upload config enabled.
|
||||
*
|
||||
* Should run on top level fields only.
|
||||
*/
|
||||
export const sanitizeUploadFields = (fields: Field[], config: CollectionConfig) => {
|
||||
if (config.upload && typeof config.upload === 'object') {
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i]
|
||||
|
||||
if (fieldAffectsData(field) && field.name) {
|
||||
if (reservedBaseUploadFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tabs without a name
|
||||
if (field.type === 'tabs') {
|
||||
for (let j = 0; j < field.tabs.length; j++) {
|
||||
const tab = field.tabs[j]
|
||||
|
||||
if (!('name' in tab)) {
|
||||
sanitizeUploadFields(tab.fields, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle presentational fields like rows and collapsibles
|
||||
if (!fieldAffectsData(field) && 'fields' in field && field.fields) {
|
||||
sanitizeUploadFields(field.fields, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { isPlainObject } from '../../utilities/isPlainObject.js'
|
||||
import baseVersionFields from '../../versions/baseFields.js'
|
||||
import { versionDefaults } from '../../versions/defaults.js'
|
||||
import { authDefaults, defaults, loginWithUsernameDefaults } from './defaults.js'
|
||||
import { sanitizeAuthFields, sanitizeUploadFields } from './reservedFieldNames.js'
|
||||
|
||||
export const sanitizeCollection = async (
|
||||
config: Config,
|
||||
@@ -38,6 +39,7 @@ export const sanitizeCollection = async (
|
||||
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
sanitized.fields = await sanitizeFields({
|
||||
collectionConfig: sanitized,
|
||||
config,
|
||||
fields: sanitized.fields,
|
||||
richTextSanitizationPromises,
|
||||
@@ -115,6 +117,9 @@ export const sanitizeCollection = async (
|
||||
if (sanitized.upload) {
|
||||
if (sanitized.upload === true) sanitized.upload = {}
|
||||
|
||||
// sanitize fields for reserved names
|
||||
sanitizeUploadFields(sanitized.fields, sanitized)
|
||||
|
||||
// disable duplicate for uploads by default
|
||||
sanitized.disableDuplicate = sanitized.disableDuplicate || true
|
||||
|
||||
@@ -133,6 +138,9 @@ export const sanitizeCollection = async (
|
||||
}
|
||||
|
||||
if (sanitized.auth) {
|
||||
// sanitize fields for reserved names
|
||||
sanitizeAuthFields(sanitized.fields, sanitized)
|
||||
|
||||
sanitized.auth = merge(authDefaults, typeof sanitized.auth === 'object' ? sanitized.auth : {}, {
|
||||
isMergeableObject: isPlainObject,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
// This gets dynamically reassigned during compilation
|
||||
export let APIErrorName = 'APIError'
|
||||
|
||||
class ExtendableError<TData extends object = { [key: string]: unknown }> extends Error {
|
||||
data: TData
|
||||
|
||||
@@ -14,6 +17,7 @@ class ExtendableError<TData extends object = { [key: string]: unknown }> extends
|
||||
// show data in cause
|
||||
cause: data,
|
||||
})
|
||||
APIErrorName = this.constructor.name
|
||||
this.name = this.constructor.name
|
||||
this.message = message
|
||||
this.status = status
|
||||
|
||||
@@ -5,6 +5,9 @@ import httpStatus from 'http-status'
|
||||
|
||||
import { APIError } from './APIError.js'
|
||||
|
||||
// This gets dynamically reassigned during compilation
|
||||
export let ValidationErrorName = 'ValidationError'
|
||||
|
||||
export class ValidationError extends APIError<{
|
||||
collection?: string
|
||||
errors: { field: string; message: string }[]
|
||||
@@ -25,5 +28,7 @@ export class ValidationError extends APIError<{
|
||||
httpStatus.BAD_REQUEST,
|
||||
results,
|
||||
)
|
||||
|
||||
ValidationErrorName = this.constructor.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { APIError } from './APIError.js'
|
||||
export { APIError, APIErrorName } from './APIError.js'
|
||||
export { AuthenticationError } from './AuthenticationError.js'
|
||||
export { DuplicateCollection } from './DuplicateCollection.js'
|
||||
export { DuplicateFieldName } from './DuplicateFieldName.js'
|
||||
@@ -19,4 +19,4 @@ export { MissingFile } from './MissingFile.js'
|
||||
export { NotFound } from './NotFound.js'
|
||||
export { QueryError } from './QueryError.js'
|
||||
export { ReservedFieldName } from './ReservedFieldName.js'
|
||||
export { ValidationError } from './ValidationError.js'
|
||||
export { ValidationError, ValidationErrorName } from './ValidationError.js'
|
||||
|
||||
@@ -9,12 +9,7 @@ import type {
|
||||
TextField,
|
||||
} from './types.js'
|
||||
|
||||
import {
|
||||
InvalidFieldName,
|
||||
InvalidFieldRelationship,
|
||||
MissingFieldType,
|
||||
ReservedFieldName,
|
||||
} from '../../errors/index.js'
|
||||
import { InvalidFieldName, InvalidFieldRelationship, MissingFieldType } from '../../errors/index.js'
|
||||
import { sanitizeFields } from './sanitize.js'
|
||||
|
||||
describe('sanitizeFields', () => {
|
||||
@@ -52,23 +47,6 @@ describe('sanitizeFields', () => {
|
||||
}).rejects.toThrow(InvalidFieldName)
|
||||
})
|
||||
|
||||
it('should throw on a reserved field name', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'hash',
|
||||
type: 'text',
|
||||
label: 'hash',
|
||||
},
|
||||
]
|
||||
await expect(async () => {
|
||||
await sanitizeFields({
|
||||
config,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
}).rejects.toThrow(ReservedFieldName)
|
||||
})
|
||||
|
||||
describe('auto-labeling', () => {
|
||||
it('should populate label if missing', async () => {
|
||||
const fields: Field[] = [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CollectionConfig } from '../../collections/config/types.js'
|
||||
import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
import type { Field } from './types.js'
|
||||
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
InvalidFieldName,
|
||||
InvalidFieldRelationship,
|
||||
MissingFieldType,
|
||||
ReservedFieldName,
|
||||
} from '../../errors/index.js'
|
||||
import { deepMerge } from '../../utilities/deepMerge.js'
|
||||
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
|
||||
@@ -18,6 +18,7 @@ import validations from '../validations.js'
|
||||
import { fieldAffectsData, tabHasName } from './types.js'
|
||||
|
||||
type Args = {
|
||||
collectionConfig?: CollectionConfig
|
||||
config: Config
|
||||
existingFieldNames?: Set<string>
|
||||
fields: Field[]
|
||||
@@ -40,9 +41,8 @@ type Args = {
|
||||
validRelationships: null | string[]
|
||||
}
|
||||
|
||||
export const reservedFieldNames = ['__v', 'salt', 'hash', 'file']
|
||||
|
||||
export const sanitizeFields = async ({
|
||||
collectionConfig,
|
||||
config,
|
||||
existingFieldNames = new Set(),
|
||||
fields,
|
||||
@@ -62,11 +62,6 @@ export const sanitizeFields = async ({
|
||||
throw new InvalidFieldName(field, field.name)
|
||||
}
|
||||
|
||||
// assert that field names are not one of reserved names
|
||||
if (fieldAffectsData(field) && reservedFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
|
||||
// Auto-label
|
||||
if (
|
||||
'name' in field &&
|
||||
@@ -116,10 +111,12 @@ export const sanitizeFields = async ({
|
||||
}
|
||||
|
||||
if (field.type === 'blocks' && field.blocks) {
|
||||
field.blocks = field.blocks.map((block) => ({
|
||||
...block,
|
||||
fields: block.fields.concat(baseBlockFields),
|
||||
}))
|
||||
field.blocks = field.blocks.map((block) => {
|
||||
return {
|
||||
...block,
|
||||
fields: block.fields.concat(baseBlockFields),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (field.type === 'array' && field.fields) {
|
||||
|
||||
@@ -841,6 +841,7 @@ export type {
|
||||
export type { EmailAdapter as PayloadEmailAdapter, SendEmailOptions } from './email/types.js'
|
||||
export {
|
||||
APIError,
|
||||
APIErrorName,
|
||||
AuthenticationError,
|
||||
DuplicateCollection,
|
||||
DuplicateFieldName,
|
||||
@@ -861,6 +862,7 @@ export {
|
||||
NotFound,
|
||||
QueryError,
|
||||
ValidationError,
|
||||
ValidationErrorName,
|
||||
} from './errors/index.js'
|
||||
export { baseBlockFields } from './fields/baseFields/baseBlockFields.js'
|
||||
export { baseIDField } from './fields/baseFields/baseIDField.js'
|
||||
|
||||
@@ -62,6 +62,16 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"test": "echo \"No tests available.\""
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"find-node-modules": "^2.1.3",
|
||||
"range-parser": "^1.2.1"
|
||||
|
||||
@@ -30,6 +30,16 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"test": "jest"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cognito-identity": "^3.525.0",
|
||||
"@aws-sdk/client-s3": "^3.525.0",
|
||||
|
||||
@@ -46,6 +46,16 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"test": "echo \"No tests available.\""
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"deepmerge": "^4.2.2",
|
||||
|
||||
@@ -36,6 +36,16 @@
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"payload": "workspace:*"
|
||||
|
||||
@@ -46,6 +46,16 @@
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/express": "^4.17.9",
|
||||
|
||||
@@ -37,6 +37,16 @@
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"payload": "workspace:*"
|
||||
|
||||
@@ -43,6 +43,16 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"test": "echo \"Error: no tests specified\""
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"deepmerge": "4.3.1"
|
||||
|
||||
@@ -37,6 +37,16 @@
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "^7.55.2",
|
||||
"@sentry/types": "^7.54.0",
|
||||
|
||||
@@ -51,6 +51,16 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/next": "workspace:*",
|
||||
|
||||
@@ -48,6 +48,16 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"lodash.get": "^4.4.2",
|
||||
|
||||
@@ -40,6 +40,16 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"translateNewKeys": "tsx scripts/translateNewKeys.ts"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lexical/headless": "0.16.1",
|
||||
"@lexical/link": "0.16.1",
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-hotkey": "0.2.0",
|
||||
"slate": "0.91.4",
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^1.1.0",
|
||||
"@azure/storage-blob": "^12.11.0",
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/storage": "^7.7.0",
|
||||
"@payloadcms/plugin-cloud-storage": "workspace:*"
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.525.0",
|
||||
"@aws-sdk/lib-storage": "^3.525.0",
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/plugin-cloud-storage": "workspace:*",
|
||||
"uploadthing": "^6.10.1"
|
||||
|
||||
@@ -31,6 +31,16 @@
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/plugin-cloud-storage": "workspace:*",
|
||||
"@vercel/blob": "^0.22.3"
|
||||
|
||||
@@ -37,6 +37,16 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"translateNewKeys": "tsx scripts/translateNewKeys/run.ts"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
"*.{md,mdx,yml,json}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"date-fns": "3.3.1"
|
||||
},
|
||||
|
||||
@@ -259,7 +259,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
|
||||
'upload:crop',
|
||||
'upload:cropToolDescription',
|
||||
'upload:dragAndDrop',
|
||||
'upload:addImage',
|
||||
'upload:addFile',
|
||||
'upload:editImage',
|
||||
'upload:focalPoint',
|
||||
'upload:focalPointDescription',
|
||||
|
||||
@@ -317,7 +317,7 @@ export const arTranslations: DefaultTranslationsObject = {
|
||||
within: 'في غضون',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'إضافة صورة',
|
||||
addFile: 'إضافة ملف',
|
||||
crop: 'محصول',
|
||||
cropToolDescription: 'اسحب الزوايا المحددة للمنطقة، رسم منطقة جديدة أو قم بضبط القيم أدناه.',
|
||||
dragAndDrop: 'قم بسحب وإسقاط ملفّ',
|
||||
|
||||
@@ -320,7 +320,7 @@ export const azTranslations: DefaultTranslationsObject = {
|
||||
within: 'daxilinde',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Şəkil əlavə et',
|
||||
addFile: 'Fayl əlavə et',
|
||||
crop: 'Məhsul',
|
||||
cropToolDescription:
|
||||
'Seçilmiş sahənin köşələrini sürükləyin, yeni bir sahə çəkin və ya aşağıdakı dəyərləri düzəltin.',
|
||||
|
||||
@@ -318,7 +318,7 @@ export const bgTranslations: DefaultTranslationsObject = {
|
||||
within: 'в рамките на',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Добавяне на изображение',
|
||||
addFile: 'Добавяне на файл',
|
||||
crop: 'Изрязване',
|
||||
cropToolDescription:
|
||||
'Плъзни ъглите на избраната област, избери нова област или коригирай стойностите по-долу.',
|
||||
|
||||
@@ -318,7 +318,7 @@ export const csTranslations: DefaultTranslationsObject = {
|
||||
within: 'uvnitř',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Přidat obrázek',
|
||||
addFile: 'Přidat soubor',
|
||||
crop: 'Ořez',
|
||||
cropToolDescription:
|
||||
'Přetáhněte rohy vybrané oblasti, nakreslete novou oblast nebo upravte níže uvedené hodnoty.',
|
||||
|
||||
@@ -324,7 +324,7 @@ export const deTranslations: DefaultTranslationsObject = {
|
||||
within: 'innerhalb',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Bild hinzufügen',
|
||||
addFile: 'Datei hinzufügen',
|
||||
crop: 'Zuschneiden',
|
||||
cropToolDescription:
|
||||
'Ziehen Sie die Ecken des ausgewählten Bereichs, zeichnen Sie einen neuen Bereich oder passen Sie die Werte unten an.',
|
||||
|
||||
@@ -321,7 +321,7 @@ export const enTranslations = {
|
||||
within: 'within',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Add Image',
|
||||
addFile: 'Add File',
|
||||
crop: 'Crop',
|
||||
cropToolDescription:
|
||||
'Drag the corners of the selected area, draw a new area or adjust the values below.',
|
||||
|
||||
@@ -323,7 +323,7 @@ export const esTranslations: DefaultTranslationsObject = {
|
||||
within: 'dentro de',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Añadir imagen',
|
||||
addFile: 'Añadir archivo',
|
||||
crop: 'Cultivo',
|
||||
cropToolDescription:
|
||||
'Arrastra las esquinas del área seleccionada, dibuja un nuevo área o ajusta los valores a continuación.',
|
||||
|
||||
@@ -318,7 +318,7 @@ export const faTranslations: DefaultTranslationsObject = {
|
||||
within: 'در داخل',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'اضافه کردن تصویر',
|
||||
addFile: 'اضافه کردن فایل',
|
||||
crop: 'محصول',
|
||||
cropToolDescription:
|
||||
'گوشههای منطقه انتخاب شده را بکشید، یک منطقه جدید رسم کنید یا مقادیر زیر را تنظیم کنید.',
|
||||
|
||||
@@ -327,7 +327,7 @@ export const frTranslations: DefaultTranslationsObject = {
|
||||
within: 'dans',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Ajouter une image',
|
||||
addFile: 'Ajouter un fichier',
|
||||
crop: 'Recadrer',
|
||||
cropToolDescription:
|
||||
'Faites glisser les coins de la zone sélectionnée, dessinez une nouvelle zone ou ajustez les valeurs ci-dessous.',
|
||||
|
||||
@@ -313,7 +313,7 @@ export const heTranslations: DefaultTranslationsObject = {
|
||||
within: 'בתוך',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'הוסף תמונה',
|
||||
addFile: 'הוסף קובץ',
|
||||
crop: 'חתוך',
|
||||
cropToolDescription: 'גרור את הפינות של האזור שנבחר, צייר אזור חדש או התאם את הערכים למטה.',
|
||||
dragAndDrop: 'גרור ושחרר קובץ',
|
||||
|
||||
@@ -319,7 +319,7 @@ export const hrTranslations: DefaultTranslationsObject = {
|
||||
within: 'unutar',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Dodaj sliku',
|
||||
addFile: 'Dodaj datoteku',
|
||||
crop: 'Usjev',
|
||||
cropToolDescription:
|
||||
'Povucite kutove odabranog područja, nacrtajte novo područje ili prilagodite vrijednosti ispod.',
|
||||
|
||||
@@ -321,7 +321,7 @@ export const huTranslations: DefaultTranslationsObject = {
|
||||
within: 'belül',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Kép hozzáadása',
|
||||
addFile: 'Fájl hozzáadása',
|
||||
crop: 'Termés',
|
||||
cropToolDescription:
|
||||
'Húzza a kijelölt terület sarkait, rajzoljon új területet, vagy igazítsa a lentebb található értékeket.',
|
||||
|
||||
@@ -321,7 +321,7 @@ export const itTranslations: DefaultTranslationsObject = {
|
||||
within: "all'interno",
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Aggiungi immagine',
|
||||
addFile: 'Aggiungi file',
|
||||
crop: 'Raccolto',
|
||||
cropToolDescription:
|
||||
"Trascina gli angoli dell'area selezionata, disegna una nuova area o regola i valori qui sotto.",
|
||||
|
||||
@@ -319,7 +319,7 @@ export const jaTranslations: DefaultTranslationsObject = {
|
||||
within: '内で',
|
||||
},
|
||||
upload: {
|
||||
addImage: '画像を追加',
|
||||
addFile: 'ファイルを追加',
|
||||
crop: 'クロップ',
|
||||
cropToolDescription:
|
||||
'選択したエリアのコーナーをドラッグしたり、新たなエリアを描画したり、下記の値を調整してください。',
|
||||
|
||||
@@ -318,7 +318,7 @@ export const koTranslations: DefaultTranslationsObject = {
|
||||
within: '내에서',
|
||||
},
|
||||
upload: {
|
||||
addImage: '이미지 추가',
|
||||
addFile: '파일 추가',
|
||||
crop: '자르기',
|
||||
cropToolDescription:
|
||||
'선택한 영역의 모퉁이를 드래그하거나 새로운 영역을 그리거나 아래의 값을 조정하세요.',
|
||||
|
||||
@@ -322,7 +322,7 @@ export const myTranslations: DefaultTranslationsObject = {
|
||||
within: 'အတွင်း',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'ပုံ ထည့်ပါ',
|
||||
addFile: 'ဖိုင်ထည့်ပါ',
|
||||
crop: 'သုန်း',
|
||||
cropToolDescription:
|
||||
'ရွေးထားသည့်ဧရိယာတွင်မွေးလျှက်မှုများကိုဆွဲပြီး, အသစ်တည်ပြီးသို့မဟုတ်အောက်ပါတ',
|
||||
|
||||
@@ -319,7 +319,7 @@ export const nbTranslations: DefaultTranslationsObject = {
|
||||
within: 'innen',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Legg til bilde',
|
||||
addFile: 'Legg til fil',
|
||||
crop: 'Beskjær',
|
||||
cropToolDescription:
|
||||
'Dra hjørnene av det valgte området, tegn et nytt område eller juster verdiene nedenfor.',
|
||||
|
||||
@@ -321,7 +321,7 @@ export const nlTranslations: DefaultTranslationsObject = {
|
||||
within: 'binnen',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Afbeelding toevoegen',
|
||||
addFile: 'Bestand toevoegen',
|
||||
crop: 'Bijsnijden',
|
||||
cropToolDescription:
|
||||
'Sleep de hoeken van het geselecteerde gebied, teken een nieuw gebied of pas de waarden hieronder aan.',
|
||||
|
||||
@@ -319,7 +319,7 @@ export const plTranslations: DefaultTranslationsObject = {
|
||||
within: 'w ciągu',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Dodaj obraz',
|
||||
addFile: 'Dodaj plik',
|
||||
crop: 'Przytnij',
|
||||
cropToolDescription:
|
||||
'Przeciągnij narożniki wybranego obszaru, narysuj nowy obszar lub dostosuj poniższe wartości.',
|
||||
|
||||
@@ -320,7 +320,7 @@ export const ptTranslations: DefaultTranslationsObject = {
|
||||
within: 'dentro',
|
||||
},
|
||||
upload: {
|
||||
addImage: 'Adicionar imagem',
|
||||
addFile: 'Adicionar arquivo',
|
||||
crop: 'Cultura',
|
||||
cropToolDescription:
|
||||
'Arraste as bordas da área selecionada, desenhe uma nova área ou ajuste os valores abaixo.',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user