feat(plugin-multi-tenant): filter users list and tenants lists (#11417)

### What?
- Adds `users` base list filtering when tenant is selected
- Adds `tenants` base list filtering when tenant is selected
This commit is contained in:
Jarrod Flesch
2025-02-26 21:50:36 -05:00
committed by GitHub
parent 67b7a730ba
commit 45cee23add
8 changed files with 142 additions and 22 deletions

View File

@@ -52,7 +52,7 @@ The plugin accepts an object with the following properties:
```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
@@ -176,6 +176,14 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
* Opt out of adding access constraints to the tenants collection
*/
useTenantsCollectionAccess?: boolean
/**
* Opt out including the baseListFilter to filter tenants by selected tenant
*/
useTenantsListFilter?: boolean
/**
* Opt out including the baseListFilter to filter users by selected tenant
*/
useUsersTenantFilter?: boolean
}
```

View File

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

View File

@@ -6,9 +6,12 @@ import { defaults } from './defaults.js'
import { tenantField } from './fields/tenantField/index.js'
import { tenantsArrayField } from './fields/tenantsArrayField/index.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 { addCollectionAccess } from './utilities/addCollectionAccess.js'
import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js'
import { withTenantListFilter } from './utilities/withTenantListFilter.js'
import { combineListFilters } from './utilities/combineListFilters.js'
export const multiTenantPlugin =
<ConfigType>(pluginConfig: MultiTenantPluginConfig<ConfigType>) =>
@@ -97,6 +100,23 @@ export const multiTenantPlugin =
userHasAccessToAllTenants,
})
if (pluginConfig.useUsersTenantFilter !== false) {
if (!adminUsersCollection.admin) {
adminUsersCollection.admin = {}
}
adminUsersCollection.admin.baseListFilter = combineListFilters({
baseListFilter: adminUsersCollection.admin?.baseListFilter,
customFilter: (args) =>
filterUsersBySelectedTenant({
req: args.req,
tenantsArrayFieldName,
tenantsArrayTenantFieldName,
tenantsCollectionSlug,
}),
})
}
let tenantCollection: CollectionConfig | undefined
const [collectionSlugs, globalCollectionSlugs] = Object.keys(pluginConfig.collections).reduce<
@@ -138,6 +158,25 @@ export const multiTenantPlugin =
})
}
if (pluginConfig.useTenantsListFilter !== false) {
/**
* Add list filter to tenants collection
* - filter by selected tenant
*/
if (!collection.admin) {
collection.admin = {}
}
collection.admin.baseListFilter = combineListFilters({
baseListFilter: collection.admin?.baseListFilter,
customFilter: (args) =>
filterTenantsBySelectedTenant({
req: args.req,
tenantsCollectionSlug,
}),
})
}
if (pluginConfig.cleanupAfterTenantDelete !== false) {
/**
* Add cleanup logic when tenant is deleted
@@ -195,10 +234,15 @@ export const multiTenantPlugin =
if (!collection.admin) {
collection.admin = {}
}
collection.admin.baseListFilter = withTenantListFilter({
collection.admin.baseListFilter = combineListFilters({
baseListFilter: collection.admin?.baseListFilter,
customFilter: (args) =>
filterDocumentsBySelectedTenant({
req: args.req,
tenantFieldName,
tenantsCollectionSlug,
}),
})
}

View File

@@ -1,15 +1,15 @@
import type { PayloadRequest, Where } from 'payload'
import { SELECT_ALL } from '../constants.js'
import { getCollectionIDType } from './getCollectionIDType.js'
import { getTenantFromCookie } from './getTenantFromCookie.js'
import { getCollectionIDType } from '../utilities/getCollectionIDType.js'
import { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
type Args = {
req: PayloadRequest
tenantFieldName: string
tenantsCollectionSlug: string
}
export const getTenantListFilter = ({
export const filterDocumentsBySelectedTenant = ({
req,
tenantFieldName,
tenantsCollectionSlug,

View File

@@ -0,0 +1,30 @@
import type { PayloadRequest, Where } from 'payload'
import { SELECT_ALL } from '../constants.js'
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 === SELECT_ALL) {
return {}
}
return {
id: {
equals: selectedTenant,
},
}
}

View File

@@ -0,0 +1,37 @@
import type { PayloadRequest, Where } from 'payload'
import { SELECT_ALL } from '../constants.js'
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 === SELECT_ALL) {
return {}
}
return {
[`${tenantsArrayFieldName}.${tenantsArrayTenantFieldName}`]: {
in: [selectedTenant],
},
}
}

View File

@@ -125,6 +125,14 @@ export type MultiTenantPluginConfig<ConfigTypes = unknown> = {
* Opt out of adding access constraints to the tenants collection
*/
useTenantsCollectionAccess?: boolean
/**
* Opt out including the baseListFilter to filter tenants by selected tenant
*/
useTenantsListFilter?: boolean
/**
* Opt out including the baseListFilter to filter users by selected tenant
*/
useUsersTenantFilter?: boolean
}
export type Tenant<IDType = number | string> = {

View File

@@ -1,19 +1,16 @@
import type { BaseListFilter, Where } from 'payload'
import { getTenantListFilter } from './getTenantListFilter.js'
type Args = {
baseListFilter?: BaseListFilter
tenantFieldName: string
tenantsCollectionSlug: string
customFilter: BaseListFilter
}
/**
* Combines a base list filter with a tenant list filter
*
* Combines where constraints inside of an AND operator
*/
export const withTenantListFilter =
({ baseListFilter, tenantFieldName, tenantsCollectionSlug }: Args): BaseListFilter =>
export const combineListFilters =
({ baseListFilter, customFilter }: Args): BaseListFilter =>
async (args) => {
const filterConstraints = []
@@ -25,14 +22,10 @@ export const withTenantListFilter =
}
}
const tenantListFilter = getTenantListFilter({
req: args.req,
tenantFieldName,
tenantsCollectionSlug,
})
const customFilterResult = await customFilter(args)
if (tenantListFilter) {
filterConstraints.push(tenantListFilter)
if (customFilterResult) {
filterConstraints.push(customFilterResult)
}
if (filterConstraints.length) {