chore(examples): multi tenant single domain fixes (#7922)

This commit is contained in:
Jarrod Flesch
2024-08-28 11:34:22 -04:00
committed by GitHub
parent c7e7dc71d3
commit e4ef47b938
14 changed files with 684 additions and 659 deletions

View File

@@ -5,6 +5,8 @@ import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views' import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = { type Args = {
params: { params: {
segments: string[] segments: string[]
@@ -17,6 +19,7 @@ type Args = {
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> => export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams }) generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams }) const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, importMap, params, searchParams })
export default NotFound export default NotFound

View File

@@ -5,6 +5,8 @@ import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { RootPage, generatePageMetadata } from '@payloadcms/next/views' import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = { type Args = {
params: { params: {
segments: string[] segments: string[]
@@ -17,6 +19,7 @@ type Args = {
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> => export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams }) generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams }) const Page = ({ params, searchParams }: Args) =>
RootPage({ config, importMap, params, searchParams })
export default Page export default Page

View File

@@ -0,0 +1,7 @@
import { TenantFieldComponent as TenantFieldComponent_0 } from '@/fields/TenantField/components/Field'
import { TenantSelectorRSC as TenantSelectorRSC_1 } from '@/components/TenantSelector'
export const importMap = {
'@/fields/TenantField/components/Field#TenantFieldComponent': TenantFieldComponent_0,
'@/components/TenantSelector#TenantSelectorRSC': TenantSelectorRSC_1,
}

View File

@@ -1,18 +1,21 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import configPromise from "@payload-config"; import configPromise from '@payload-config'
import "@payloadcms/next/css"; import '@payloadcms/next/css'
import { RootLayout } from "@payloadcms/next/layouts"; import { RootLayout } from '@payloadcms/next/layouts'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import React from "react"; import React from 'react'
import "./custom.scss"; import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = { type Args = {
children: React.ReactNode; children: React.ReactNode
}; }
const Layout = ({ children }: Args) => ( const Layout = ({ children }: Args) => (
<RootLayout config={configPromise}>{children}</RootLayout> <RootLayout config={configPromise} importMap={importMap}>
); {children}
</RootLayout>
)
export default Layout; export default Layout

View File

@@ -1,6 +1,6 @@
import type { Access } from 'payload' import type { Access } from 'payload'
import type { User } from '../../../../payload-types' import type { User } from '../../../payload-types'
import { isSuperAdmin } from '../../../access/isSuperAdmin' import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs' import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'

View File

@@ -1,6 +1,6 @@
import type { CollectionConfig } from 'payload' import type { CollectionConfig } from 'payload'
import type { User } from '../../../payload-types' import type { User } from '../../payload-types'
import { getTenantAdminTenantAccessIDs } from '../../utilities/getTenantAccessIDs' import { getTenantAdminTenantAccessIDs } from '../../utilities/getTenantAccessIDs'
import { createAccess } from './access/create' import { createAccess } from './access/create'
@@ -37,32 +37,6 @@ const Users: CollectionConfig = {
{ {
name: 'tenant', name: 'tenant',
type: 'relationship', type: 'relationship',
filterOptions: ({ user }) => {
if (!user) {
// Would like to query where exists true on id
// but that is not working
return {
id: {
like: '',
},
}
}
if (user?.roles?.includes('super-admin')) {
// Would like to query where exists true on id
// but that is not working
return {
id: {
like: '',
},
}
}
const adminTenantAccessIDs = getTenantAdminTenantAccessIDs(user as User)
return {
id: {
in: adminTenantAccessIDs,
},
}
},
index: true, index: true,
relationTo: 'tenants', relationTo: 'tenants',
required: true, required: true,

View File

@@ -2,17 +2,18 @@
import type { Option } from '@payloadcms/ui/elements/ReactSelect' import type { Option } from '@payloadcms/ui/elements/ReactSelect'
import type { OptionObject } from 'payload' import type { OptionObject } from 'payload'
import { getTenantAdminTenantAccessIDs } from '@/utilities/getTenantAccessIDs'
import { SelectInput, useAuth } from '@payloadcms/ui' import { SelectInput, useAuth } from '@payloadcms/ui'
import * as qs from 'qs-esm'
import React from 'react' import React from 'react'
import type { Tenant, User } from '../../../payload-types.js' import type { Tenant, User } from '../../payload-types'
import './index.scss' import './index.scss'
export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) => { export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) => {
const { user } = useAuth<User>() const { user } = useAuth<User>()
const [options, setOptions] = React.useState<OptionObject[]>([]) const [options, setOptions] = React.useState<OptionObject[]>([])
const [value, setValue] = React.useState<string | undefined>(initialCookie)
const isSuperAdmin = user?.roles?.includes('super-admin') const isSuperAdmin = user?.roles?.includes('super-admin')
const tenantIDs = const tenantIDs =
@@ -28,18 +29,6 @@ export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) =>
document.cookie = name + '=' + (value || '') + expires + '; path=/' document.cookie = name + '=' + (value || '') + expires + '; path=/'
} }
React.useEffect(() => {
const fetchTenants = async () => {
const res = await fetch(`/api/tenants?depth=0&limit=100&sort=name`, {
credentials: 'include',
}).then((res) => res.json())
setOptions(res.docs.map((doc: Tenant) => ({ label: doc.name, value: doc.id })))
}
void fetchTenants()
}, [])
const handleChange = React.useCallback((option: Option | Option[]) => { const handleChange = React.useCallback((option: Option | Option[]) => {
if (!option) { if (!option) {
setCookie('payload-tenant', undefined) setCookie('payload-tenant', undefined)
@@ -50,7 +39,44 @@ export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) =>
} }
}, []) }, [])
if (isSuperAdmin || tenantIDs.length > 1) { React.useEffect(() => {
const fetchTenants = async () => {
const adminOfTenants = getTenantAdminTenantAccessIDs(user ?? null)
const queryString = qs.stringify(
{
depth: 0,
limit: 100,
sort: 'name',
where: {
id: {
in: adminOfTenants,
},
},
},
{
addQueryPrefix: true,
},
)
const res = await fetch(`/api/tenants${queryString}`, {
credentials: 'include',
}).then((res) => res.json())
const optionsToSet = res.docs.map((doc: Tenant) => ({ label: doc.name, value: doc.id }))
if (optionsToSet.length === 1) {
setCookie('payload-tenant', optionsToSet[0].value)
}
setOptions(optionsToSet)
}
if (user) {
void fetchTenants()
}
}, [user])
if ((isSuperAdmin || tenantIDs.length > 1) && options.length > 1) {
return ( return (
<div className="tenant-selector"> <div className="tenant-selector">
<SelectInput <SelectInput
@@ -59,7 +85,7 @@ export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) =>
onChange={handleChange} onChange={handleChange}
options={options} options={options}
path="setTenant" path="setTenant"
value={value} value={options.find((opt) => opt.value === initialCookie)?.value}
/> />
</div> </div>
) )

View File

@@ -0,0 +1,34 @@
'use client'
import { RelationshipField, useField } from '@payloadcms/ui'
import React from 'react'
type Props = {
initialValue?: string
path: string
readOnly: boolean
}
export function TenantFieldComponentClient({ initialValue, path, readOnly }: Props) {
const { formInitializing, setValue } = useField({ path })
const hasSetInitialValue = React.useRef(false)
React.useEffect(() => {
if (!hasSetInitialValue.current && !formInitializing && initialValue) {
setValue(initialValue)
hasSetInitialValue.current = true
}
}, [initialValue, setValue, formInitializing])
return (
<RelationshipField
field={{
name: path,
type: 'relationship',
_path: path,
label: 'Tenant',
relationTo: 'tenants',
required: true,
}}
readOnly={readOnly}
/>
)
}

View File

@@ -1,26 +1,32 @@
'use client' import type { Payload } from 'payload'
import { RelationshipField, useAuth, useFieldProps } from '@payloadcms/ui'
import { cookies as getCookies, headers as getHeaders } from 'next/headers'
import React from 'react' import React from 'react'
import type { User } from '../../../../payload-types.js' import { TenantFieldComponentClient } from './Field.client'
export const TenantFieldComponent = () => { export const TenantFieldComponent: React.FC<{
const { user } = useAuth<User>() path: string
const { path, readOnly } = useFieldProps() payload: Payload
readOnly: boolean
}> = async (args) => {
const cookies = getCookies()
const headers = getHeaders()
const { user } = await args.payload.auth({ headers })
if (user) { if (
if ((user.tenants && user.tenants.length > 1) || user?.roles?.includes('super-admin')) { user &&
return ( ((Array.isArray(user.tenants) && user.tenants.length > 1) ||
<RelationshipField user?.roles?.includes('super-admin'))
label="Tenant" ) {
name={path} return (
path={path} <TenantFieldComponentClient
readOnly={readOnly} initialValue={cookies.get('payload-tenant')?.value || undefined}
relationTo="tenants" path={args.path}
required readOnly={args.readOnly}
/> />
) )
}
} }
return null return null
} }

View File

@@ -2,7 +2,6 @@ import type { Field } from 'payload'
import { isSuperAdmin } from '../../access/isSuperAdmin' import { isSuperAdmin } from '../../access/isSuperAdmin'
import { tenantFieldUpdate } from './access/update' import { tenantFieldUpdate } from './access/update'
import { TenantFieldComponent } from './components/Field'
import { autofillTenant } from './hooks/autofillTenant' import { autofillTenant } from './hooks/autofillTenant'
export const tenantField: Field = { export const tenantField: Field = {
@@ -17,7 +16,7 @@ export const tenantField: Field = {
}, },
admin: { admin: {
components: { components: {
Field: TenantFieldComponent, Field: '@/fields/TenantField/components/Field#TenantFieldComponent',
}, },
position: 'sidebar', position: 'sidebar',
}, },

View File

@@ -17,6 +17,9 @@ export interface Config {
'payload-preferences': PayloadPreference; 'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration; 'payload-migrations': PayloadMigration;
}; };
db: {
defaultIDType: string;
};
globals: {}; globals: {};
locale: null; locale: null;
user: User & { user: User & {
@@ -26,15 +29,20 @@ export interface Config {
export interface UserAuthOperations { export interface UserAuthOperations {
forgotPassword: { forgotPassword: {
email: string; email: string;
password: string;
}; };
login: { login: {
password: string;
email: string; email: string;
password: string;
}; };
registerFirstUser: { registerFirstUser: {
email: string; email: string;
password: string; password: string;
}; };
unlock: {
email: string;
password: string;
};
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema

View File

@@ -7,7 +7,6 @@ import { fileURLToPath } from 'url'
import { Pages } from './collections/Pages' import { Pages } from './collections/Pages'
import { Tenants } from './collections/Tenants' import { Tenants } from './collections/Tenants'
import Users from './collections/Users' import Users from './collections/Users'
import { TenantSelectorRSC } from './components/TenantSelector'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
@@ -15,7 +14,7 @@ const dirname = path.dirname(filename)
export default buildConfig({ export default buildConfig({
admin: { admin: {
components: { components: {
afterNavLinks: [TenantSelectorRSC], afterNavLinks: ['@/components/TenantSelector#TenantSelectorRSC'],
}, },
user: 'users', user: 'users',
}, },

View File

@@ -1,4 +1,4 @@
import type { User } from '../../payload-types' import type { User } from '../payload-types'
export const getTenantAccessIDs = (user: User | null): string[] => { export const getTenantAccessIDs = (user: User | null): string[] => {
if (!user) return [] if (!user) return []

File diff suppressed because it is too large Load Diff