fix(plugin-multi-tenant): constrain results to assigned tenants when present (#13365)

Extension of https://github.com/payloadcms/payload/pull/13213

This PR correctly filters tenants, users and documents based on the
users assigned tenants if any are set. If a user is assigned tenants
then list results should only show documents with those tenants (when
selector is not set). Previously you could construct access results that
allows them to see them, but in the confines of the admin panel they
should not see them. If you wanted a user to be able to see a "public"
tenant while inside the admin panel they either need to be added to the
tenant or have no tenants at all.

Note that this is for filtering only, access control still controls what
documents a user has _access_ to a document. The filters are and always
have been a way to filter out results in the list view.
This commit is contained in:
Jarrod Flesch
2025-08-05 09:00:36 -04:00
committed by GitHub
parent 43b4b22af9
commit 20b4de94ee
13 changed files with 521 additions and 234 deletions

View File

@@ -1,4 +1,4 @@
export { filterDocumentsBySelectedTenant as getTenantListFilter } from '../list-filters/filterDocumentsBySelectedTenant.js'
export { filterDocumentsByTenants as getTenantListFilter } from '../filters/filterDocumentsByTenants.js'
export { getGlobalViewRedirect } from '../utilities/getGlobalViewRedirect.js'
export { getTenantAccess } from '../utilities/getTenantAccess.js'
export { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'

View File

@@ -0,0 +1,52 @@
import type { PayloadRequest, Where } from 'payload'
import { defaults } from '../defaults.js'
import { getCollectionIDType } from '../utilities/getCollectionIDType.js'
import { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
import { getUserTenantIDs } from '../utilities/getUserTenantIDs.js'
type Args = {
filterFieldName: string
req: PayloadRequest
tenantsArrayFieldName?: string
tenantsArrayTenantFieldName?: string
tenantsCollectionSlug: string
}
export const filterDocumentsByTenants = ({
filterFieldName,
req,
tenantsArrayFieldName = defaults.tenantsArrayFieldName,
tenantsArrayTenantFieldName = defaults.tenantsArrayTenantFieldName,
tenantsCollectionSlug,
}: Args): null | Where => {
const idType = getCollectionIDType({
collectionSlug: tenantsCollectionSlug,
payload: req.payload,
})
// scope results to selected tenant
const selectedTenant = getTenantFromCookie(req.headers, idType)
if (selectedTenant) {
return {
[filterFieldName]: {
in: [selectedTenant],
},
}
}
// scope to user assigned tenants
const userAssignedTenants = getUserTenantIDs(req.user, {
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
})
if (userAssignedTenants.length > 0) {
return {
[filterFieldName]: {
in: userAssignedTenants,
},
}
}
// no tenant selected and no user tenants, return null to allow access control to handle it
return null
}

View File

@@ -10,10 +10,8 @@ import { defaults } from './defaults.js'
import { getTenantOptionsEndpoint } from './endpoints/getTenantOptionsEndpoint.js'
import { tenantField } from './fields/tenantField/index.js'
import { tenantsArrayField } from './fields/tenantsArrayField/index.js'
import { filterDocumentsByTenants } from './filters/filterDocumentsByTenants.js'
import { addTenantCleanup } from './hooks/afterTenantDelete.js'
import { filterDocumentsBySelectedTenant } from './list-filters/filterDocumentsBySelectedTenant.js'
import { filterTenantsBySelectedTenant } from './list-filters/filterTenantsBySelectedTenant.js'
import { filterUsersBySelectedTenant } from './list-filters/filterUsersBySelectedTenant.js'
import { translations } from './translations/index.js'
import { addCollectionAccess } from './utilities/addCollectionAccess.js'
import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js'
@@ -148,7 +146,8 @@ export const multiTenantPlugin =
adminUsersCollection.admin.baseListFilter = combineListFilters({
baseListFilter: adminUsersCollection.admin?.baseListFilter,
customFilter: (args) =>
filterUsersBySelectedTenant({
filterDocumentsByTenants({
filterFieldName: `${tenantsArrayFieldName}.${tenantsArrayTenantFieldName}`,
req: args.req,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
@@ -211,8 +210,11 @@ export const multiTenantPlugin =
collection.admin.baseListFilter = combineListFilters({
baseListFilter: collection.admin?.baseListFilter,
customFilter: (args) =>
filterTenantsBySelectedTenant({
filterDocumentsByTenants({
filterFieldName: 'id',
req: args.req,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
tenantsCollectionSlug,
}),
})
@@ -306,9 +308,11 @@ export const multiTenantPlugin =
collection.admin.baseListFilter = combineListFilters({
baseListFilter: collection.admin?.baseListFilter,
customFilter: (args) =>
filterDocumentsBySelectedTenant({
filterDocumentsByTenants({
filterFieldName: tenantFieldName,
req: args.req,
tenantFieldName,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
tenantsCollectionSlug,
}),
})

View File

@@ -1,31 +0,0 @@
import type { PayloadRequest, Where } from 'payload'
import { getCollectionIDType } from '../utilities/getCollectionIDType.js'
import { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
type Args = {
req: PayloadRequest
tenantFieldName: string
tenantsCollectionSlug: string
}
export const filterDocumentsBySelectedTenant = ({
req,
tenantFieldName,
tenantsCollectionSlug,
}: Args): null | Where => {
const idType = getCollectionIDType({
collectionSlug: tenantsCollectionSlug,
payload: req.payload,
})
const selectedTenant = getTenantFromCookie(req.headers, idType)
if (selectedTenant) {
return {
[tenantFieldName]: {
equals: selectedTenant,
},
}
}
return {}
}

View File

@@ -1,29 +0,0 @@
import type { PayloadRequest, Where } from 'payload'
import { getCollectionIDType } from '../utilities/getCollectionIDType.js'
import { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
type Args = {
req: PayloadRequest
tenantsCollectionSlug: string
}
export const filterTenantsBySelectedTenant = ({
req,
tenantsCollectionSlug,
}: Args): null | Where => {
const idType = getCollectionIDType({
collectionSlug: tenantsCollectionSlug,
payload: req.payload,
})
const selectedTenant = getTenantFromCookie(req.headers, idType)
if (selectedTenant) {
return {
id: {
equals: selectedTenant,
},
}
}
return {}
}

View File

@@ -1,36 +0,0 @@
import type { PayloadRequest, Where } from 'payload'
import { getCollectionIDType } from '../utilities/getCollectionIDType.js'
import { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
type Args = {
req: PayloadRequest
tenantsArrayFieldName: string
tenantsArrayTenantFieldName: string
tenantsCollectionSlug: string
}
/**
* Filter the list of users by the selected tenant
*/
export const filterUsersBySelectedTenant = ({
req,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
tenantsCollectionSlug,
}: Args): null | Where => {
const idType = getCollectionIDType({
collectionSlug: tenantsCollectionSlug,
payload: req.payload,
})
const selectedTenant = getTenantFromCookie(req.headers, idType)
if (selectedTenant) {
return {
[`${tenantsArrayFieldName}.${tenantsArrayTenantFieldName}`]: {
in: [selectedTenant],
},
}
}
return {}
}

View File

@@ -19,10 +19,9 @@ export const getUserTenantIDs = <IDType extends number | string>(
return []
}
const {
tenantsArrayFieldName = defaults.tenantsArrayFieldName,
tenantsArrayTenantFieldName = defaults.tenantsArrayTenantFieldName,
} = options || {}
const tenantsArrayFieldName = options?.tenantsArrayFieldName || defaults.tenantsArrayFieldName
const tenantsArrayTenantFieldName =
options?.tenantsArrayTenantFieldName || defaults.tenantsArrayTenantFieldName
return (
(Array.isArray(user[tenantsArrayFieldName]) ? user[tenantsArrayFieldName] : [])?.reduce<

View File

@@ -1,9 +1,85 @@
import type { CollectionConfig } from 'payload'
import type { Access, CollectionConfig, Where } from 'payload'
import { getUserTenantIDs } from '@payloadcms/plugin-multi-tenant/utilities'
import { menuItemsSlug } from '../shared.js'
const collectionTenantReadAccess: Access = ({ req }) => {
// admins can access all tenants
if (req?.user?.roles?.includes('admin')) {
return true
}
if (req.user) {
const assignedTenants = getUserTenantIDs(req.user, {
tenantsArrayFieldName: 'tenants',
tenantsArrayTenantFieldName: 'tenant',
})
// if the user has assigned tenants, add id constraint
if (assignedTenants.length > 0) {
return {
or: [
{
tenant: {
in: assignedTenants,
},
},
{
'tenant.isPublic': {
equals: true,
},
},
],
}
}
}
// if the user has no assigned tenants, return a filter that allows access to public tenants
return {
'tenant.isPublic': {
equals: true,
},
} as Where
}
const collectionTenantUpdateAccess: Access = ({ req }) => {
// admins can update all tenants
if (req?.user?.roles?.includes('admin')) {
return true
}
if (req.user) {
const assignedTenants = getUserTenantIDs(req.user, {
tenantsArrayFieldName: 'tenants',
tenantsArrayTenantFieldName: 'tenant',
})
// if the user has assigned tenants, add id constraint
if (assignedTenants.length > 0) {
return {
tenant: {
in: assignedTenants,
},
}
}
}
return false
}
export const MenuItems: CollectionConfig = {
slug: menuItemsSlug,
access: {
read: collectionTenantReadAccess,
create: ({ req }) => {
return Boolean(req?.user?.roles?.includes('admin'))
},
update: collectionTenantUpdateAccess,
delete: ({ req }) => {
return Boolean(req?.user?.roles?.includes('admin'))
},
},
admin: {
useAsTitle: 'name',
group: 'Tenant Collections',

View File

@@ -1,9 +1,56 @@
import type { CollectionConfig } from 'payload'
import type { Access, CollectionConfig, Where } from 'payload'
import { getUserTenantIDs } from '@payloadcms/plugin-multi-tenant/utilities'
import { tenantsSlug } from '../shared.js'
const tenantAccess: Access = ({ req }) => {
// admins can access all tenants
if (req?.user?.roles?.includes('admin')) {
return true
}
if (req.user) {
const assignedTenants = getUserTenantIDs(req.user, {
tenantsArrayFieldName: 'tenants',
tenantsArrayTenantFieldName: 'tenant',
})
// if the user has assigned tenants, add id constraint
if (assignedTenants.length > 0) {
return {
or: [
{
id: {
in: assignedTenants,
},
},
{
isPublic: {
equals: true,
},
},
],
}
}
}
// if the user has no assigned tenants, return a filter that allows access to public tenants
return {
isPublic: {
equals: true,
},
} as Where
}
export const Tenants: CollectionConfig = {
slug: tenantsSlug,
access: {
read: tenantAccess,
create: tenantAccess,
update: tenantAccess,
delete: tenantAccess,
},
labels: {
singular: 'Tenant',
plural: 'Tenants',
@@ -30,5 +77,10 @@ export const Tenants: CollectionConfig = {
collection: 'users',
on: 'tenants.tenant',
},
{
name: 'isPublic',
type: 'checkbox',
label: 'Public Tenant',
},
],
}

View File

@@ -32,11 +32,14 @@ export default buildConfigWithDefaults({
plugins: [
multiTenantPlugin<ConfigType>({
userHasAccessToAllTenants: (user) => Boolean(user.roles?.includes('admin')),
useTenantsCollectionAccess: false,
tenantField: {
access: {},
},
collections: {
[menuItemsSlug]: {},
[menuItemsSlug]: {
useTenantAccess: false,
},
[menuSlug]: {
isGlobal: true,
},

View File

@@ -26,7 +26,7 @@ import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { reInitializeDB } from '../helpers/reInitializeDB.js'
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
import { credentials } from './credentials.js'
import { menuItemsSlug, menuSlug, tenantsSlug } from './shared.js'
import { menuItemsSlug, menuSlug, tenantsSlug, usersSlug } from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -36,6 +36,7 @@ test.describe('Multi Tenant', () => {
let serverURL: string
let globalMenuURL: AdminUrlUtil
let menuItemsURL: AdminUrlUtil
let usersURL: AdminUrlUtil
let tenantsURL: AdminUrlUtil
test.beforeAll(async ({ browser }, testInfo) => {
@@ -45,6 +46,7 @@ test.describe('Multi Tenant', () => {
serverURL = serverFromInit
globalMenuURL = new AdminUrlUtil(serverURL, menuSlug)
menuItemsURL = new AdminUrlUtil(serverURL, menuItemsSlug)
usersURL = new AdminUrlUtil(serverURL, usersSlug)
tenantsURL = new AdminUrlUtil(serverURL, tenantsSlug)
const context = await browser.newContext()
@@ -60,72 +62,202 @@ test.describe('Multi Tenant', () => {
})
})
test.describe('tenant selector', () => {
test('should populate tenant selector on login', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await expect
.poll(async () => {
return (await getTenantOptions({ page })).sort()
test.describe('Filters', () => {
test.describe('Tenants', () => {
test('should show all tenants when tenant selector is empty', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
.toEqual(['Blue Dog', 'Steel Cat', 'Anchor Bar'].sort())
await clearTenant({ page })
await page.goto(tenantsURL.list)
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Blue Dog',
}),
).toBeVisible()
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Steel Cat',
}),
).toBeVisible()
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Public Tenant',
}),
).toBeVisible()
})
test('should show filtered tenants when tenant selector is set', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await selectTenant({
page,
tenant: 'Blue Dog',
})
await page.goto(tenantsURL.list)
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Blue Dog',
}),
).toBeVisible()
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Steel Cat',
}),
).toBeHidden()
})
})
test('should populate the tenant selector after logout with 1 tenant user', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.blueDog,
})
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await expect
.poll(async () => {
return (await getTenantOptions({ page })).sort()
test.describe('Tenant Assigned Documents', () => {
test('should show all tenant items when tenant selector is empty', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
.toEqual(['Blue Dog', 'Steel Cat', 'Anchor Bar'].sort())
await clearTenant({ page })
await page.goto(menuItemsURL.list)
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Spicy Mac',
}),
).toBeVisible()
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Pretzel Bites',
}),
).toBeVisible()
})
test('should show filtered tenant items when tenant selector is set', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await selectTenant({
page,
tenant: 'Blue Dog',
})
await page.goto(menuItemsURL.list)
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Spicy Mac',
}),
).toBeVisible()
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Pretzel Bites',
}),
).toBeHidden()
})
test('should show public tenant items to super admins', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await clearTenant({ page })
await page.goto(menuItemsURL.list)
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Free Pizza',
}),
).toBeVisible()
})
test('should not show public tenant items to users with assigned tenants', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.owner,
})
await clearTenant({ page })
await page.goto(menuItemsURL.list)
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Free Pizza',
}),
).toBeHidden()
})
})
test('should show all tenants for userHasAccessToAllTenants users', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
test.describe('Users', () => {
test('should show all users when tenant selector is empty', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await clearTenant({ page })
await page.goto(usersURL.list)
await expect(
page.locator('.collection-list .table .cell-email', {
hasText: 'jane@blue-dog.com',
}),
).toBeVisible()
await expect(
page.locator('.collection-list .table .cell-email', {
hasText: 'huel@steel-cat.com',
}),
).toBeVisible()
await expect(
page.locator('.collection-list .table .cell-email', {
hasText: 'dev@payloadcms.com',
}),
).toBeVisible()
})
await expect
.poll(async () => {
return (await getTenantOptions({ page })).sort()
test('should show only tenant users when tenant selector is empty', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
.toEqual(['Blue Dog', 'Steel Cat', 'Anchor Bar'].sort())
})
test('should only show users assigned tenants', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.owner,
await selectTenant({
page,
tenant: 'Blue Dog',
})
await page.goto(usersURL.list)
await expect(
page.locator('.collection-list .table .cell-email', {
hasText: 'jane@blue-dog.com',
}),
).toBeVisible()
await expect(
page.locator('.collection-list .table .cell-email', {
hasText: 'huel@steel-cat.com',
}),
).toBeHidden()
await expect(
page.locator('.collection-list .table .cell-email', {
hasText: 'dev@payloadcms.com',
}),
).toBeHidden()
})
await expect
.poll(async () => {
return (await getTenantOptions({ page })).sort()
})
.toEqual(['Blue Dog', 'Anchor Bar'].sort())
})
})
test.describe('Base List Filter', () => {
test('should show all tenant items when tenant selector is empty', async () => {
test.describe('Documents', () => {
test('should set tenant upon entering document', async () => {
await loginClientSide({
page,
serverURL,
@@ -134,45 +266,54 @@ test.describe('Multi Tenant', () => {
await clearTenant({ page })
await page.goto(menuItemsURL.list)
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Spicy Mac',
}),
).toBeVisible()
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Pretzel Bites',
}),
).toBeVisible()
await goToListDoc({
page,
cellClass: '.cell-name',
textToMatch: 'Spicy Mac',
urlUtil: menuItemsURL,
})
await openNav(page)
await expect
.poll(async () => {
return await getSelectInputValue<false>({
selectLocator: page.locator('.tenant-selector'),
multiSelect: false,
})
})
.toBe('Blue Dog')
})
test('should show filtered tenant items when tenant selector is set', async () => {
test('should prompt for confirmation upon tenant switching', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await selectTenant({
await clearTenant({ page })
await goToListDoc({
page,
tenant: 'Blue Dog',
cellClass: '.cell-name',
textToMatch: 'Spicy Mac',
urlUtil: menuItemsURL,
})
await page.goto(menuItemsURL.list)
await selectTenant({
page,
tenant: 'Steel Cat',
})
const confirmationModal = page.locator('#confirm-switch-tenant')
await expect(confirmationModal).toBeVisible()
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Spicy Mac',
}),
confirmationModal.getByText('You are about to change ownership from Blue Dog to Steel Cat'),
).toBeVisible()
await expect(
page.locator('.collection-list .table .cell-name', {
hasText: 'Pretzel Bites',
}),
).toBeHidden()
})
})
test.describe('globals', () => {
test.describe('Globals', () => {
test('should redirect list view to edit view', async () => {
await loginClientSide({
page,
@@ -229,68 +370,98 @@ test.describe('Multi Tenant', () => {
await confirmationModal.locator('#confirm-action').click()
await expect(page.locator('#confirm-leave-without-saving')).toBeHidden()
await page.locator('#nav-food-items').click()
})
})
test.describe('documents', () => {
test('should set tenant upon entering', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await clearTenant({ page })
await goToListDoc({
page,
cellClass: '.cell-name',
textToMatch: 'Spicy Mac',
urlUtil: menuItemsURL,
})
await openNav(page)
await page.goto(menuItemsURL.list)
await expect
.poll(async () => {
return await getSelectInputValue<false>({
return await getSelectInputValue({
selectLocator: page.locator('.tenant-selector'),
multiSelect: false,
})
})
.toBe('Blue Dog')
.toBe('Steel Cat')
})
})
test('should prompt for confirmation upon tenant switching', async () => {
test.describe('Tenant Selector', () => {
test('should populate tenant selector on login', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await expect
.poll(async () => {
return (await getTenantOptions({ page })).sort()
})
.toEqual(['Blue Dog', 'Steel Cat', 'Public Tenant', 'Anchor Bar'].sort())
})
test('should populate the tenant selector after logout with 1 tenant user', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.blueDog,
})
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await expect
.poll(async () => {
return (await getTenantOptions({ page })).sort()
})
.toEqual(['Blue Dog', 'Steel Cat', 'Public Tenant', 'Anchor Bar'].sort())
})
test('should show all tenants for userHasAccessToAllTenants users', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await expect
.poll(async () => {
return (await getTenantOptions({ page })).sort()
})
.toEqual(['Blue Dog', 'Steel Cat', 'Public Tenant', 'Anchor Bar'].sort())
})
test('should only show users assigned tenants', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.owner,
})
await expect
.poll(async () => {
return (await getTenantOptions({ page })).sort()
})
.toEqual(['Blue Dog', 'Anchor Bar'].sort())
})
test('should not show public tenants to users with assigned tenants', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.owner,
})
await clearTenant({ page })
await goToListDoc({
page,
cellClass: '.cell-name',
textToMatch: 'Spicy Mac',
urlUtil: menuItemsURL,
})
await selectTenant({
page,
tenant: 'Steel Cat',
})
const confirmationModal = page.locator('#confirm-switch-tenant')
await expect(confirmationModal).toBeVisible()
await page.goto(tenantsURL.list)
await expect(
confirmationModal.getByText('You are about to change ownership from Blue Dog to Steel Cat'),
).toBeVisible()
page.locator('.collection-list .table .cell-name', {
hasText: 'Public Tenant',
}),
).toBeHidden()
})
})
test.describe('tenants', () => {
test('should update the tenant name in the selector when editing a tenant', async () => {
await loginClientSide({
page,
@@ -314,7 +485,7 @@ test.describe('Multi Tenant', () => {
.poll(async () => {
return (await getTenantOptions({ page })).sort()
})
.toEqual(['Red Dog', 'Steel Cat', 'Anchor Bar'].sort())
.toEqual(['Red Dog', 'Steel Cat', 'Public Tenant', 'Anchor Bar'].sort())
// Change the tenant back to the original name
await page.locator('#field-name').fill('Blue Dog')
@@ -323,7 +494,7 @@ test.describe('Multi Tenant', () => {
.poll(async () => {
return (await getTenantOptions({ page })).sort()
})
.toEqual(['Blue Dog', 'Steel Cat', 'Anchor Bar'].sort())
.toEqual(['Blue Dog', 'Steel Cat', 'Public Tenant', 'Anchor Bar'].sort())
})
test('should add tenant to the selector when creating a new tenant', async () => {
@@ -347,7 +518,7 @@ test.describe('Multi Tenant', () => {
.poll(async () => {
return (await getTenantOptions({ page })).sort()
})
.toEqual(['Blue Dog', 'Steel Cat', 'Anchor Bar', 'House Rules'].sort())
.toEqual(['Blue Dog', 'Steel Cat', 'Anchor Bar', 'Public Tenant', 'House Rules'].sort())
})
})
})

View File

@@ -134,6 +134,7 @@ export interface Tenant {
hasNextPage?: boolean;
totalDocs?: number;
};
isPublic?: boolean | null;
updatedAt: string;
createdAt: string;
}
@@ -274,6 +275,7 @@ export interface TenantsSelect<T extends boolean = true> {
name?: T;
domain?: T;
users?: T;
isPublic?: T;
updatedAt?: T;
createdAt?: T;
}

View File

@@ -26,6 +26,14 @@ export const seed: Config['onInit'] = async (payload) => {
domain: 'anchorbar.com',
},
})
const publicTenant = await payload.create({
collection: tenantsSlug,
data: {
name: 'Public Tenant',
domain: 'public.com',
isPublic: true,
},
})
// Create blue dog menu items
await payload.create({
@@ -73,6 +81,22 @@ export const seed: Config['onInit'] = async (payload) => {
},
})
// Public tenant menu items
await payload.create({
collection: menuItemsSlug,
data: {
name: 'Free Pizza',
tenant: publicTenant.id,
},
})
await payload.create({
collection: menuItemsSlug,
data: {
name: 'Free Dogs',
tenant: publicTenant.id,
},
})
// create users
await payload.create({
collection: usersSlug,