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 ```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = { type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/** /**
* After a tenant is deleted, the plugin will attempt to clean up related documents * After a tenant is deleted, the plugin will attempt to clean up related documents
* - removing documents with the tenant ID * - removing documents with the tenant ID
* - removing the tenant from users * - removing the tenant from users
@@ -176,6 +176,14 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
* Opt out of adding access constraints to the tenants collection * Opt out of adding access constraints to the tenants collection
*/ */
useTenantsCollectionAccess?: boolean 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 { getGlobalViewRedirect } from '../utilities/getGlobalViewRedirect.js'
export { getTenantAccess } from '../utilities/getTenantAccess.js' export { getTenantAccess } from '../utilities/getTenantAccess.js'
export { getTenantFromCookie } from '../utilities/getTenantFromCookie.js' export { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
export { getTenantListFilter } from '../utilities/getTenantListFilter.js'
export { getUserTenantIDs } from '../utilities/getUserTenantIDs.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 { tenantField } from './fields/tenantField/index.js'
import { tenantsArrayField } from './fields/tenantsArrayField/index.js' import { tenantsArrayField } from './fields/tenantsArrayField/index.js'
import { addTenantCleanup } from './hooks/afterTenantDelete.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 { addCollectionAccess } from './utilities/addCollectionAccess.js'
import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js' import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js'
import { withTenantListFilter } from './utilities/withTenantListFilter.js' import { combineListFilters } from './utilities/combineListFilters.js'
export const multiTenantPlugin = export const multiTenantPlugin =
<ConfigType>(pluginConfig: MultiTenantPluginConfig<ConfigType>) => <ConfigType>(pluginConfig: MultiTenantPluginConfig<ConfigType>) =>
@@ -97,6 +100,23 @@ export const multiTenantPlugin =
userHasAccessToAllTenants, 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 let tenantCollection: CollectionConfig | undefined
const [collectionSlugs, globalCollectionSlugs] = Object.keys(pluginConfig.collections).reduce< 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) { if (pluginConfig.cleanupAfterTenantDelete !== false) {
/** /**
* Add cleanup logic when tenant is deleted * Add cleanup logic when tenant is deleted
@@ -195,10 +234,15 @@ export const multiTenantPlugin =
if (!collection.admin) { if (!collection.admin) {
collection.admin = {} collection.admin = {}
} }
collection.admin.baseListFilter = withTenantListFilter({
collection.admin.baseListFilter = combineListFilters({
baseListFilter: collection.admin?.baseListFilter, baseListFilter: collection.admin?.baseListFilter,
tenantFieldName, customFilter: (args) =>
tenantsCollectionSlug, filterDocumentsBySelectedTenant({
req: args.req,
tenantFieldName,
tenantsCollectionSlug,
}),
}) })
} }

View File

@@ -1,15 +1,15 @@
import type { PayloadRequest, Where } from 'payload' import type { PayloadRequest, Where } from 'payload'
import { SELECT_ALL } from '../constants.js' import { SELECT_ALL } from '../constants.js'
import { getCollectionIDType } from './getCollectionIDType.js' import { getCollectionIDType } from '../utilities/getCollectionIDType.js'
import { getTenantFromCookie } from './getTenantFromCookie.js' import { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
type Args = { type Args = {
req: PayloadRequest req: PayloadRequest
tenantFieldName: string tenantFieldName: string
tenantsCollectionSlug: string tenantsCollectionSlug: string
} }
export const getTenantListFilter = ({ export const filterDocumentsBySelectedTenant = ({
req, req,
tenantFieldName, tenantFieldName,
tenantsCollectionSlug, 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 * Opt out of adding access constraints to the tenants collection
*/ */
useTenantsCollectionAccess?: boolean 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> = { export type Tenant<IDType = number | string> = {

View File

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