Skip to content

Commit e29d1d9

Browse files
fix(plugin-multi-tenant): prefer assigned tenants for selector population (#13213)
When populating the selector it should populate it with assigned tenants before fetching all tenants that a user has access to. You may have "public" tenants and while a user may have _access_ to the tenant, the selector should show the ones they are assigned to. Users with full access are the ones that should be able to see the public ones for editing.
1 parent 4ac428d commit e29d1d9

File tree

8 files changed

+212
-80
lines changed

8 files changed

+212
-80
lines changed

packages/plugin-multi-tenant/src/components/GlobalViewRedirect/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@ import type { CollectionSlug, ServerProps, ViewTypes } from 'payload'
33
import { headers as getHeaders } from 'next/headers.js'
44
import { redirect } from 'next/navigation.js'
55

6+
import type { MultiTenantPluginConfig } from '../../types.js'
7+
68
import { getGlobalViewRedirect } from '../../utilities/getGlobalViewRedirect.js'
79

810
type Args = {
911
basePath?: string
1012
collectionSlug: CollectionSlug
1113
docID?: number | string
1214
globalSlugs: string[]
15+
tenantArrayFieldName: string
16+
tenantArrayTenantFieldName: string
1317
tenantFieldName: string
1418
tenantsCollectionSlug: string
1519
useAsTitle: string
20+
userHasAccessToAllTenants: Required<MultiTenantPluginConfig<any>>['userHasAccessToAllTenants']
1621
viewType: ViewTypes
1722
} & ServerProps
1823

@@ -27,9 +32,12 @@ export const GlobalViewRedirect = async (args: Args) => {
2732
headers,
2833
payload: args.payload,
2934
tenantFieldName: args.tenantFieldName,
35+
tenantsArrayFieldName: args.tenantArrayFieldName,
36+
tenantsArrayTenantFieldName: args.tenantArrayTenantFieldName,
3037
tenantsCollectionSlug: args.tenantsCollectionSlug,
3138
useAsTitle: args.useAsTitle,
3239
user: args.user,
40+
userHasAccessToAllTenants: args.userHasAccessToAllTenants,
3341
view: args.viewType,
3442
})
3543

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Endpoint } from 'payload'
2+
3+
import { APIError } from 'payload'
4+
5+
import type { MultiTenantPluginConfig } from '../types.js'
6+
7+
import { getTenantOptions } from '../utilities/getTenantOptions.js'
8+
9+
export const getTenantOptionsEndpoint = <ConfigType>({
10+
tenantsArrayFieldName,
11+
tenantsArrayTenantFieldName,
12+
tenantsCollectionSlug,
13+
useAsTitle,
14+
userHasAccessToAllTenants,
15+
}: {
16+
tenantsArrayFieldName: string
17+
tenantsArrayTenantFieldName: string
18+
tenantsCollectionSlug: string
19+
useAsTitle: string
20+
userHasAccessToAllTenants: Required<
21+
MultiTenantPluginConfig<ConfigType>
22+
>['userHasAccessToAllTenants']
23+
}): Endpoint => ({
24+
handler: async (req) => {
25+
const { payload, user } = req
26+
27+
if (!user) {
28+
throw new APIError('Unauthorized', 401)
29+
}
30+
31+
const tenantOptions = await getTenantOptions({
32+
payload,
33+
tenantsArrayFieldName,
34+
tenantsArrayTenantFieldName,
35+
tenantsCollectionSlug,
36+
useAsTitle,
37+
user,
38+
userHasAccessToAllTenants,
39+
})
40+
41+
return new Response(JSON.stringify({ tenantOptions }))
42+
},
43+
method: 'get',
44+
path: '/populate-tenant-options',
45+
})

packages/plugin-multi-tenant/src/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { PluginDefaultTranslationsObject } from './translations/types.js'
77
import type { MultiTenantPluginConfig } from './types.js'
88

99
import { defaults } from './defaults.js'
10+
import { getTenantOptionsEndpoint } from './endpoints/getTenantOptionsEndpoint.js'
1011
import { tenantField } from './fields/tenantField/index.js'
1112
import { tenantsArrayField } from './fields/tenantsArrayField/index.js'
1213
import { addTenantCleanup } from './hooks/afterTenantDelete.js'
@@ -248,6 +249,17 @@ export const multiTenantPlugin =
248249
},
249250
},
250251
})
252+
253+
collection.endpoints = [
254+
...(collection.endpoints || []),
255+
getTenantOptionsEndpoint<ConfigType>({
256+
tenantsArrayFieldName,
257+
tenantsArrayTenantFieldName,
258+
tenantsCollectionSlug,
259+
useAsTitle: tenantCollection.admin?.useAsTitle || 'id',
260+
userHasAccessToAllTenants,
261+
}),
262+
]
251263
} else if (pluginConfig.collections?.[collection.slug]) {
252264
const isGlobal = Boolean(pluginConfig.collections[collection.slug]?.isGlobal)
253265

@@ -327,8 +339,11 @@ export const multiTenantPlugin =
327339
*/
328340
incomingConfig.admin.components.providers.push({
329341
clientProps: {
342+
tenantsArrayFieldName,
343+
tenantsArrayTenantFieldName,
330344
tenantsCollectionSlug: tenantCollection.slug,
331345
useAsTitle: tenantCollection.admin?.useAsTitle || 'id',
346+
userHasAccessToAllTenants,
332347
},
333348
path: '@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider',
334349
})
@@ -343,8 +358,11 @@ export const multiTenantPlugin =
343358
basePath,
344359
globalSlugs: globalCollectionSlugs,
345360
tenantFieldName,
361+
tenantsArrayFieldName,
362+
tenantsArrayTenantFieldName,
346363
tenantsCollectionSlug,
347364
useAsTitle: tenantCollection.admin?.useAsTitle || 'id',
365+
userHasAccessToAllTenants,
348366
},
349367
})
350368
}

packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,14 @@ const Context = createContext<ContextType>({
6565

6666
export const TenantSelectionProviderClient = ({
6767
children,
68+
initialTenantOptions,
6869
initialValue,
69-
tenantCookie,
70-
tenantOptions: tenantOptionsFromProps,
7170
tenantsCollectionSlug,
72-
useAsTitle,
7371
}: {
7472
children: React.ReactNode
73+
initialTenantOptions: OptionObject[]
7574
initialValue?: number | string
76-
tenantCookie?: string
77-
tenantOptions: OptionObject[]
7875
tenantsCollectionSlug: string
79-
useAsTitle: string
8076
}) => {
8177
const [selectedTenantID, setSelectedTenantID] = React.useState<number | string | undefined>(
8278
initialValue,
@@ -89,7 +85,7 @@ export const TenantSelectionProviderClient = ({
8985
const prevUserID = React.useRef(userID)
9086
const userChanged = userID !== prevUserID.current
9187
const [tenantOptions, setTenantOptions] = React.useState<OptionObject[]>(
92-
() => tenantOptionsFromProps,
88+
() => initialTenantOptions,
9389
)
9490
const selectedTenantLabel = React.useMemo(
9591
() => tenantOptions.find((option) => option.value === selectedTenantID)?.label,
@@ -142,7 +138,7 @@ export const TenantSelectionProviderClient = ({
142138
const syncTenants = React.useCallback(async () => {
143139
try {
144140
const req = await fetch(
145-
`${config.serverURL}${config.routes.api}/${tenantsCollectionSlug}?select[${useAsTitle}]=true&limit=0&depth=0`,
141+
`${config.serverURL}${config.routes.api}/${tenantsCollectionSlug}/populate-tenant-options`,
146142
{
147143
credentials: 'include',
148144
method: 'GET',
@@ -151,23 +147,18 @@ export const TenantSelectionProviderClient = ({
151147

152148
const result = await req.json()
153149

154-
if (result.docs && userID) {
155-
setTenantOptions(
156-
result.docs.map((doc: Record<string, number | string>) => ({
157-
label: doc[useAsTitle],
158-
value: doc.id,
159-
})),
160-
)
161-
162-
if (result.totalDocs === 1) {
163-
setSelectedTenantID(result.docs[0].id)
164-
setCookie(String(result.docs[0].id))
150+
if (result.tenantOptions && userID) {
151+
setTenantOptions(result.tenantOptions)
152+
153+
if (result.tenantOptions.length === 1) {
154+
setSelectedTenantID(result.tenantOptions[0].value)
155+
setCookie(String(result.tenantOptions[0].value))
165156
}
166157
}
167158
} catch (e) {
168159
toast.error(`Error fetching tenants`)
169160
}
170-
}, [config.serverURL, config.routes.api, tenantsCollectionSlug, useAsTitle, setCookie, userID])
161+
}, [config.serverURL, config.routes.api, tenantsCollectionSlug, setCookie, userID])
171162

172163
const updateTenants = React.useCallback<ContextType['updateTenants']>(
173164
({ id, label }) => {

packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.tsx

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,47 @@
1-
import type { OptionObject, Payload, TypedUser } from 'payload'
1+
import type { Payload, TypedUser } from 'payload'
22

33
import { cookies as getCookies } from 'next/headers.js'
44

5-
import { findTenantOptions } from '../../queries/findTenantOptions.js'
5+
import type { MultiTenantPluginConfig } from '../../types.js'
6+
7+
import { getTenantOptions } from '../../utilities/getTenantOptions.js'
68
import { TenantSelectionProviderClient } from './index.client.js'
79

8-
type Args = {
10+
type Args<ConfigType> = {
911
children: React.ReactNode
1012
payload: Payload
13+
tenantsArrayFieldName: string
14+
tenantsArrayTenantFieldName: string
1115
tenantsCollectionSlug: string
1216
useAsTitle: string
1317
user: TypedUser
18+
userHasAccessToAllTenants: Required<
19+
MultiTenantPluginConfig<ConfigType>
20+
>['userHasAccessToAllTenants']
1421
}
1522

1623
export const TenantSelectionProvider = async ({
1724
children,
1825
payload,
26+
tenantsArrayFieldName,
27+
tenantsArrayTenantFieldName,
1928
tenantsCollectionSlug,
2029
useAsTitle,
2130
user,
22-
}: Args) => {
23-
let tenantOptions: OptionObject[] = []
24-
25-
try {
26-
const { docs } = await findTenantOptions({
27-
limit: 0,
28-
payload,
29-
tenantsCollectionSlug,
30-
useAsTitle,
31-
user,
32-
})
33-
tenantOptions = docs.map((doc) => ({
34-
label: String(doc[useAsTitle]),
35-
value: doc.id,
36-
}))
37-
} catch (_) {
38-
// user likely does not have access
39-
}
31+
userHasAccessToAllTenants,
32+
}: Args<any>) => {
33+
const tenantOptions = await getTenantOptions({
34+
payload,
35+
tenantsArrayFieldName,
36+
tenantsArrayTenantFieldName,
37+
tenantsCollectionSlug,
38+
useAsTitle,
39+
user,
40+
userHasAccessToAllTenants,
41+
})
4042

4143
const cookies = await getCookies()
42-
let tenantCookie = cookies.get('payload-tenant')?.value
44+
const tenantCookie = cookies.get('payload-tenant')?.value
4345
let initialValue = undefined
4446

4547
/**
@@ -56,17 +58,14 @@ export const TenantSelectionProvider = async ({
5658
* If the there was no cookie or the cookie was an invalid tenantID set intialValue
5759
*/
5860
if (!initialValue) {
59-
tenantCookie = undefined
6061
initialValue = tenantOptions.length > 1 ? undefined : tenantOptions[0]?.value
6162
}
6263

6364
return (
6465
<TenantSelectionProviderClient
66+
initialTenantOptions={tenantOptions}
6567
initialValue={initialValue}
66-
tenantCookie={tenantCookie}
67-
tenantOptions={tenantOptions}
6868
tenantsCollectionSlug={tenantsCollectionSlug}
69-
useAsTitle={useAsTitle}
7069
>
7170
{children}
7271
</TenantSelectionProviderClient>

packages/plugin-multi-tenant/src/queries/findTenantOptions.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.

packages/plugin-multi-tenant/src/utilities/getGlobalViewRedirect.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import type { Payload, TypedUser, ViewTypes } from 'payload'
22

3+
import { unauthorized } from 'next/navigation.js'
34
import { formatAdminURL } from 'payload/shared'
45

5-
import { findTenantOptions } from '../queries/findTenantOptions.js'
6+
import type { MultiTenantPluginConfig } from '../types.js'
7+
68
import { getCollectionIDType } from './getCollectionIDType.js'
79
import { getTenantFromCookie } from './getTenantFromCookie.js'
10+
import { getTenantOptions } from './getTenantOptions.js'
811

912
type Args = {
1013
basePath?: string
@@ -13,9 +16,12 @@ type Args = {
1316
payload: Payload
1417
slug: string
1518
tenantFieldName: string
19+
tenantsArrayFieldName: string
20+
tenantsArrayTenantFieldName: string
1621
tenantsCollectionSlug: string
1722
useAsTitle: string
1823
user?: TypedUser
24+
userHasAccessToAllTenants: Required<MultiTenantPluginConfig<any>>['userHasAccessToAllTenants']
1925
view: ViewTypes
2026
}
2127
export async function getGlobalViewRedirect({
@@ -25,9 +31,12 @@ export async function getGlobalViewRedirect({
2531
headers,
2632
payload,
2733
tenantFieldName,
34+
tenantsArrayFieldName,
35+
tenantsArrayTenantFieldName,
2836
tenantsCollectionSlug,
2937
useAsTitle,
3038
user,
39+
userHasAccessToAllTenants,
3140
view,
3241
}: Args): Promise<string | void> {
3342
const idType = getCollectionIDType({
@@ -37,16 +46,22 @@ export async function getGlobalViewRedirect({
3746
let tenant = getTenantFromCookie(headers, idType)
3847
let redirectRoute: `/${string}` | void = undefined
3948

49+
if (!user) {
50+
return unauthorized()
51+
}
52+
4053
if (!tenant) {
41-
const tenantsQuery = await findTenantOptions({
42-
limit: 1,
54+
const tenantOptions = await getTenantOptions({
4355
payload,
56+
tenantsArrayFieldName,
57+
tenantsArrayTenantFieldName,
4458
tenantsCollectionSlug,
4559
useAsTitle,
4660
user,
61+
userHasAccessToAllTenants,
4762
})
4863

49-
tenant = tenantsQuery.docs[0]?.id || null
64+
tenant = tenantOptions[0]?.value || null
5065
}
5166

5267
try {

0 commit comments

Comments
 (0)