feat: beginings of multi-tenant organization

This commit is contained in:
Yehoshua Sandler 2025-05-16 06:11:52 -05:00
parent c72f5cf07f
commit 880694da19
9 changed files with 379 additions and 110 deletions

12
package-lock.json generated
View File

@ -15,6 +15,7 @@
"@payloadcms/next": "3.33.0", "@payloadcms/next": "3.33.0",
"@payloadcms/payload-cloud": "3.33.0", "@payloadcms/payload-cloud": "3.33.0",
"@payloadcms/plugin-form-builder": "3.33.0", "@payloadcms/plugin-form-builder": "3.33.0",
"@payloadcms/plugin-multi-tenant": "^3.37.0",
"@payloadcms/plugin-nested-docs": "3.33.0", "@payloadcms/plugin-nested-docs": "3.33.0",
"@payloadcms/plugin-redirects": "3.33.0", "@payloadcms/plugin-redirects": "3.33.0",
"@payloadcms/plugin-search": "3.33.0", "@payloadcms/plugin-search": "3.33.0",
@ -3769,6 +3770,17 @@
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020" "react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
} }
}, },
"node_modules/@payloadcms/plugin-multi-tenant": {
"version": "3.37.0",
"resolved": "https://registry.npmjs.org/@payloadcms/plugin-multi-tenant/-/plugin-multi-tenant-3.37.0.tgz",
"integrity": "sha512-f7oCmLnHgockL5Zv/a2gz4cUWhDODj3Edm1LX9tFqIy5w4L4BkcWUVsjjdtd29EMJ3ovWlWWfFYZeGiCSpIU0g==",
"license": "MIT",
"peerDependencies": {
"@payloadcms/ui": "3.37.0",
"next": "^15.2.3",
"payload": "3.37.0"
}
},
"node_modules/@payloadcms/plugin-nested-docs": { "node_modules/@payloadcms/plugin-nested-docs": {
"version": "3.33.0", "version": "3.33.0",
"resolved": "https://registry.npmjs.org/@payloadcms/plugin-nested-docs/-/plugin-nested-docs-3.33.0.tgz", "resolved": "https://registry.npmjs.org/@payloadcms/plugin-nested-docs/-/plugin-nested-docs-3.33.0.tgz",

View File

@ -20,10 +20,12 @@
}, },
"dependencies": { "dependencies": {
"@payloadcms/admin-bar": "3.33.0", "@payloadcms/admin-bar": "3.33.0",
"@payloadcms/db-postgres": "3.33.0",
"@payloadcms/live-preview-react": "3.33.0", "@payloadcms/live-preview-react": "3.33.0",
"@payloadcms/next": "3.33.0", "@payloadcms/next": "3.33.0",
"@payloadcms/payload-cloud": "3.33.0", "@payloadcms/payload-cloud": "3.33.0",
"@payloadcms/plugin-form-builder": "3.33.0", "@payloadcms/plugin-form-builder": "3.33.0",
"@payloadcms/plugin-multi-tenant": "^3.37.0",
"@payloadcms/plugin-nested-docs": "3.33.0", "@payloadcms/plugin-nested-docs": "3.33.0",
"@payloadcms/plugin-redirects": "3.33.0", "@payloadcms/plugin-redirects": "3.33.0",
"@payloadcms/plugin-search": "3.33.0", "@payloadcms/plugin-search": "3.33.0",
@ -49,8 +51,7 @@
"react-hook-form": "7.45.4", "react-hook-form": "7.45.4",
"sharp": "0.32.6", "sharp": "0.32.6",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7"
"@payloadcms/db-postgres": "3.33.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",

14
src/access/admin.ts Normal file
View File

@ -0,0 +1,14 @@
import { UserAccessLevel } from "@/collections/Users";
import { Access } from "payload";
export const isTenantAdmin: Access = ({ req }): boolean => {
return (req.user?.accessLevel || 0) >= UserAccessLevel.TENANT_ADMIN
}
export const isSuperAdmin: Access = ({ req }): boolean => {
return (req.user?.accessLevel || 0) >= UserAccessLevel.SUPER_ADMIN
}
export const isFullAccess: Access = ({ req }): boolean => {
return (req.user?.accessLevel || 0) === UserAccessLevel.FULL_ACCESS
}

View File

@ -1,3 +1,4 @@
import { TenantField as TenantField_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
@ -23,49 +24,36 @@ import { RowLabel as RowLabel_ec255a65fa6fa8d1faeb09cf35284224 } from '@/Header/
import { RowLabel as RowLabel_1f6ff6ff633e3695d348f4f3c58f1466 } from '@/Footer/RowLabel' import { RowLabel as RowLabel_1f6ff6ff633e3695d348f4f3c58f1466 } from '@/Footer/RowLabel'
import { default as default_1a7510af427896d367a49dbf838d2de6 } from '@/components/BeforeDashboard' import { default as default_1a7510af427896d367a49dbf838d2de6 } from '@/components/BeforeDashboard'
import { default as default_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin' import { default as default_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
import { TenantSelector as TenantSelector_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
import { TenantSelectionProvider as TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
export const importMap = { export const importMap = {
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell': "@payloadcms/plugin-multi-tenant/client#TenantField": TenantField_1d0591e3cf4f332c83a86da13a0de59a,
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e, "@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalField': "@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e, "@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
'@payloadcms/richtext-lexical/rsc#LexicalDiffComponent': "@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e, "@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient': "@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient': "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#HeadingFeatureClient': "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#ParagraphFeatureClient': "@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@payloadcms/richtext-lexical/client#UnderlineFeatureClient': "@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860,
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@payloadcms/richtext-lexical/client#BoldFeatureClient': "@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@/fields/slug/SlugComponent#SlugComponent": SlugComponent_92cc057d0a2abb4f6cf0307edf59f986,
'@payloadcms/richtext-lexical/client#ItalicFeatureClient': "@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#LinkFeatureClient': "@payloadcms/plugin-search/client#LinkToDoc": LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634,
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/plugin-search/client#ReindexButton": ReindexButton_aead06e4cbf6b2620c5c51c9ab283634,
'@payloadcms/plugin-seo/client#OverviewComponent': "@/Header/RowLabel#RowLabel": RowLabel_ec255a65fa6fa8d1faeb09cf35284224,
OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860, "@/Footer/RowLabel#RowLabel": RowLabel_1f6ff6ff633e3695d348f4f3c58f1466,
'@payloadcms/plugin-seo/client#MetaTitleComponent': "@/components/BeforeDashboard#default": default_1a7510af427896d367a49dbf838d2de6,
MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860, "@/components/BeforeLogin#default": default_8a7ab0eb7ab5c511aba12e68480bfe5e,
'@payloadcms/plugin-seo/client#MetaImageComponent': "@payloadcms/plugin-multi-tenant/client#TenantSelector": TenantSelector_1d0591e3cf4f332c83a86da13a0de59a,
MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860, "@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62
'@payloadcms/plugin-seo/client#MetaDescriptionComponent':
MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@payloadcms/plugin-seo/client#PreviewComponent':
PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
'@/fields/slug/SlugComponent#SlugComponent': SlugComponent_92cc057d0a2abb4f6cf0307edf59f986,
'@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient':
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/richtext-lexical/client#BlocksFeatureClient':
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
'@payloadcms/plugin-search/client#LinkToDoc': LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634,
'@payloadcms/plugin-search/client#ReindexButton': ReindexButton_aead06e4cbf6b2620c5c51c9ab283634,
'@/Header/RowLabel#RowLabel': RowLabel_ec255a65fa6fa8d1faeb09cf35284224,
'@/Footer/RowLabel#RowLabel': RowLabel_1f6ff6ff633e3695d348f4f3c58f1466,
'@/components/BeforeDashboard#default': default_1a7510af427896d367a49dbf838d2de6,
'@/components/BeforeLogin#default': default_8a7ab0eb7ab5c511aba12e68480bfe5e,
} }

View File

@ -0,0 +1,51 @@
import type { CollectionConfig } from 'payload'
//import { updateAndDeleteAccess } from './access/updateAndDelete'
import { isSuperAdmin } from '@/access/admin'
export const Tenants: CollectionConfig = {
slug: 'tenants',
access: {
create: isSuperAdmin,
delete: isSuperAdmin, // change these to the example soon!
read: ({ req }) => true,
update: isSuperAdmin,
},
admin: {
useAsTitle: 'name',
},
fields: [
{
name: 'name',
type: 'text',
required: true,
},
{
name: 'domain',
type: 'text',
admin: {
description: 'Used for domain-based tenant handling',
},
},
{
name: 'slug',
type: 'text',
admin: {
description: 'Used for url paths, example: /tenant-slug/page-slug',
},
index: true,
required: true,
},
{
name: 'allowPublicRead',
type: 'checkbox',
admin: {
description:
'If checked, logging in is not required to read. Useful for building public pages.',
position: 'sidebar',
},
defaultValue: false,
index: true,
},
],
}

View File

@ -1,6 +1,36 @@
import type { CollectionConfig } from 'payload' import type { CollectionConfig } from 'payload'
import { authenticated } from '../../access/authenticated' import { authenticated } from '../../access/authenticated'
import { isSuperAdmin } from '@/access/admin'
import { tenantsArrayField } from '@payloadcms/plugin-multi-tenant/fields'
export enum UserAccessLevel {
GUEST = 0,
CLIENT,
TRAINER,
TENANT_ADMIN,
SUPER_ADMIN,
FULL_ACCESS,
}
const defaultTenantArrayField = tenantsArrayField({
tenantsArrayFieldName: 'tenants',
tenantsArrayTenantFieldName: 'tenant',
tenantsCollectionSlug: 'tenants',
arrayFieldAccess: {},
tenantFieldAccess: {},
rowFields: [
{
name: 'roles',
type: 'select',
defaultValue: ['tenant-viewer'],
hasMany: true,
options: ['tenant-admin', 'tenant-client', 'tenant-instructor'],
required: true,
},
],
})
export const Users: CollectionConfig = { export const Users: CollectionConfig = {
slug: 'users', slug: 'users',
@ -13,13 +43,45 @@ export const Users: CollectionConfig = {
}, },
admin: { admin: {
defaultColumns: ['name', 'email'], defaultColumns: ['name', 'email'],
useAsTitle: 'name', useAsTitle: 'username',
}, },
auth: true, auth: true,
fields: [ fields: [
{ {
name: 'name', admin: {
position: 'sidebar',
},
name: 'roles',
type: 'select',
defaultValue: ['guest'],
hasMany: true,
options: ['full-access', 'super-admin', 'user', 'guest'],
// access: {
// update: ({ req }) => {
// return isSuperAdmin({req})
// },
// },
},
{
name: 'username',
type: 'text', type: 'text',
// hooks: {
// beforeValidate: [ensureUniqueUsername],
// },
index: true,
},
{
name: 'accessLevel',
type: 'number',
max: UserAccessLevel.FULL_ACCESS,
defaultValue: UserAccessLevel.GUEST,
},
{
...defaultTenantArrayField,
admin: {
...(defaultTenantArrayField?.admin || {}),
position: 'sidebar',
},
}, },
], ],
timestamps: true, timestamps: true,

View File

@ -54,6 +54,7 @@ export type SupportedTimezones =
| 'Asia/Singapore' | 'Asia/Singapore'
| 'Asia/Tokyo' | 'Asia/Tokyo'
| 'Asia/Seoul' | 'Asia/Seoul'
| 'Australia/Brisbane'
| 'Australia/Sydney' | 'Australia/Sydney'
| 'Pacific/Guam' | 'Pacific/Guam'
| 'Pacific/Noumea' | 'Pacific/Noumea'
@ -71,6 +72,7 @@ export interface Config {
media: Media; media: Media;
categories: Category; categories: Category;
users: User; users: User;
tenants: Tenant;
redirects: Redirect; redirects: Redirect;
forms: Form; forms: Form;
'form-submissions': FormSubmission; 'form-submissions': FormSubmission;
@ -87,6 +89,7 @@ export interface Config {
media: MediaSelect<false> | MediaSelect<true>; media: MediaSelect<false> | MediaSelect<true>;
categories: CategoriesSelect<false> | CategoriesSelect<true>; categories: CategoriesSelect<false> | CategoriesSelect<true>;
users: UsersSelect<false> | UsersSelect<true>; users: UsersSelect<false> | UsersSelect<true>;
tenants: TenantsSelect<false> | TenantsSelect<true>;
redirects: RedirectsSelect<false> | RedirectsSelect<true>; redirects: RedirectsSelect<false> | RedirectsSelect<true>;
forms: FormsSelect<false> | FormsSelect<true>; forms: FormsSelect<false> | FormsSelect<true>;
'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>; 'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>;
@ -97,7 +100,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>; 'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
}; };
db: { db: {
defaultIDType: string; defaultIDType: number;
}; };
globals: { globals: {
header: Header; header: Header;
@ -145,7 +148,8 @@ export interface UserAuthOperations {
* via the `definition` "pages". * via the `definition` "pages".
*/ */
export interface Page { export interface Page {
id: string; id: number;
tenant?: (number | null) | Tenant;
title: string; title: string;
hero: { hero: {
type: 'none' | 'highImpact' | 'mediumImpact' | 'lowImpact'; type: 'none' | 'highImpact' | 'mediumImpact' | 'lowImpact';
@ -172,11 +176,11 @@ export interface Page {
reference?: reference?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@ -188,7 +192,7 @@ export interface Page {
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
media?: (string | null) | Media; media?: (number | null) | Media;
}; };
layout: (CallToActionBlock | ContentBlock | MediaBlock | ArchiveBlock | FormBlock)[]; layout: (CallToActionBlock | ContentBlock | MediaBlock | ArchiveBlock | FormBlock)[];
meta?: { meta?: {
@ -196,7 +200,7 @@ export interface Page {
/** /**
* Maximum upload file size: 12MB. Recommended file size for images is <500KB. * Maximum upload file size: 12MB. Recommended file size for images is <500KB.
*/ */
image?: (string | null) | Media; image?: (number | null) | Media;
description?: string | null; description?: string | null;
}; };
publishedAt?: string | null; publishedAt?: string | null;
@ -206,14 +210,36 @@ export interface Page {
createdAt: string; createdAt: string;
_status?: ('draft' | 'published') | null; _status?: ('draft' | 'published') | null;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tenants".
*/
export interface Tenant {
id: number;
name: string;
/**
* Used for domain-based tenant handling
*/
domain?: string | null;
/**
* Used for url paths, example: /tenant-slug/page-slug
*/
slug: string;
/**
* If checked, logging in is not required to read. Useful for building public pages.
*/
allowPublicRead?: boolean | null;
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts". * via the `definition` "posts".
*/ */
export interface Post { export interface Post {
id: string; id: number;
title: string; title: string;
heroImage?: (string | null) | Media; heroImage?: (number | null) | Media;
content: { content: {
root: { root: {
type: string; type: string;
@ -229,18 +255,18 @@ export interface Post {
}; };
[k: string]: unknown; [k: string]: unknown;
}; };
relatedPosts?: (string | Post)[] | null; relatedPosts?: (number | Post)[] | null;
categories?: (string | Category)[] | null; categories?: (number | Category)[] | null;
meta?: { meta?: {
title?: string | null; title?: string | null;
/** /**
* Maximum upload file size: 12MB. Recommended file size for images is <500KB. * Maximum upload file size: 12MB. Recommended file size for images is <500KB.
*/ */
image?: (string | null) | Media; image?: (number | null) | Media;
description?: string | null; description?: string | null;
}; };
publishedAt?: string | null; publishedAt?: string | null;
authors?: (string | User)[] | null; authors?: (number | User)[] | null;
populatedAuthors?: populatedAuthors?:
| { | {
id?: string | null; id?: string | null;
@ -258,7 +284,7 @@ export interface Post {
* via the `definition` "media". * via the `definition` "media".
*/ */
export interface Media { export interface Media {
id: string; id: number;
alt?: string | null; alt?: string | null;
caption?: { caption?: {
root: { root: {
@ -350,14 +376,14 @@ export interface Media {
* via the `definition` "categories". * via the `definition` "categories".
*/ */
export interface Category { export interface Category {
id: string; id: number;
title: string; title: string;
slug?: string | null; slug?: string | null;
slugLock?: boolean | null; slugLock?: boolean | null;
parent?: (string | null) | Category; parent?: (number | null) | Category;
breadcrumbs?: breadcrumbs?:
| { | {
doc?: (string | null) | Category; doc?: (number | null) | Category;
url?: string | null; url?: string | null;
label?: string | null; label?: string | null;
id?: string | null; id?: string | null;
@ -371,8 +397,17 @@ export interface Category {
* via the `definition` "users". * via the `definition` "users".
*/ */
export interface User { export interface User {
id: string; id: number;
name?: string | null; roles?: ('full-access' | 'super-admin' | 'user' | 'guest')[] | null;
username?: string | null;
accessLevel?: number | null;
tenants?:
| {
tenant: number | Tenant;
roles: ('tenant-admin' | 'tenant-client' | 'tenant-instructor')[];
id?: string | null;
}[]
| null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
email: string; email: string;
@ -412,11 +447,11 @@ export interface CallToActionBlock {
reference?: reference?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@ -462,11 +497,11 @@ export interface ContentBlock {
reference?: reference?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@ -487,7 +522,7 @@ export interface ContentBlock {
* via the `definition` "MediaBlock". * via the `definition` "MediaBlock".
*/ */
export interface MediaBlock { export interface MediaBlock {
media: string | Media; media: number | Media;
id?: string | null; id?: string | null;
blockName?: string | null; blockName?: string | null;
blockType: 'mediaBlock'; blockType: 'mediaBlock';
@ -514,12 +549,12 @@ export interface ArchiveBlock {
} | null; } | null;
populateBy?: ('collection' | 'selection') | null; populateBy?: ('collection' | 'selection') | null;
relationTo?: 'posts' | null; relationTo?: 'posts' | null;
categories?: (string | Category)[] | null; categories?: (number | Category)[] | null;
limit?: number | null; limit?: number | null;
selectedDocs?: selectedDocs?:
| { | {
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
}[] }[]
| null; | null;
id?: string | null; id?: string | null;
@ -531,7 +566,7 @@ export interface ArchiveBlock {
* via the `definition` "FormBlock". * via the `definition` "FormBlock".
*/ */
export interface FormBlock { export interface FormBlock {
form: string | Form; form: number | Form;
enableIntro?: boolean | null; enableIntro?: boolean | null;
introContent?: { introContent?: {
root: { root: {
@ -557,7 +592,7 @@ export interface FormBlock {
* via the `definition` "forms". * via the `definition` "forms".
*/ */
export interface Form { export interface Form {
id: string; id: number;
title: string; title: string;
fields?: fields?:
| ( | (
@ -624,6 +659,7 @@ export interface Form {
label?: string | null; label?: string | null;
width?: number | null; width?: number | null;
defaultValue?: string | null; defaultValue?: string | null;
placeholder?: string | null;
options?: options?:
| { | {
label: string; label: string;
@ -730,7 +766,7 @@ export interface Form {
* via the `definition` "redirects". * via the `definition` "redirects".
*/ */
export interface Redirect { export interface Redirect {
id: string; id: number;
/** /**
* You will need to rebuild the website when changing this field. * You will need to rebuild the website when changing this field.
*/ */
@ -740,11 +776,11 @@ export interface Redirect {
reference?: reference?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null); } | null);
url?: string | null; url?: string | null;
}; };
@ -756,8 +792,8 @@ export interface Redirect {
* via the `definition` "form-submissions". * via the `definition` "form-submissions".
*/ */
export interface FormSubmission { export interface FormSubmission {
id: string; id: number;
form: string | Form; form: number | Form;
submissionData?: submissionData?:
| { | {
field: string; field: string;
@ -775,18 +811,18 @@ export interface FormSubmission {
* via the `definition` "search". * via the `definition` "search".
*/ */
export interface Search { export interface Search {
id: string; id: number;
title?: string | null; title?: string | null;
priority?: number | null; priority?: number | null;
doc: { doc: {
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
}; };
slug?: string | null; slug?: string | null;
meta?: { meta?: {
title?: string | null; title?: string | null;
description?: string | null; description?: string | null;
image?: (string | null) | Media; image?: (number | null) | Media;
}; };
categories?: categories?:
| { | {
@ -803,7 +839,7 @@ export interface Search {
* via the `definition` "payload-jobs". * via the `definition` "payload-jobs".
*/ */
export interface PayloadJob { export interface PayloadJob {
id: string; id: number;
/** /**
* Input data provided to the job * Input data provided to the job
*/ */
@ -895,52 +931,56 @@ export interface PayloadJob {
* via the `definition` "payload-locked-documents". * via the `definition` "payload-locked-documents".
*/ */
export interface PayloadLockedDocument { export interface PayloadLockedDocument {
id: string; id: number;
document?: document?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null) } | null)
| ({ | ({
relationTo: 'media'; relationTo: 'media';
value: string | Media; value: number | Media;
} | null) } | null)
| ({ | ({
relationTo: 'categories'; relationTo: 'categories';
value: string | Category; value: number | Category;
} | null) } | null)
| ({ | ({
relationTo: 'users'; relationTo: 'users';
value: string | User; value: number | User;
} | null)
| ({
relationTo: 'tenants';
value: number | Tenant;
} | null) } | null)
| ({ | ({
relationTo: 'redirects'; relationTo: 'redirects';
value: string | Redirect; value: number | Redirect;
} | null) } | null)
| ({ | ({
relationTo: 'forms'; relationTo: 'forms';
value: string | Form; value: number | Form;
} | null) } | null)
| ({ | ({
relationTo: 'form-submissions'; relationTo: 'form-submissions';
value: string | FormSubmission; value: number | FormSubmission;
} | null) } | null)
| ({ | ({
relationTo: 'search'; relationTo: 'search';
value: string | Search; value: number | Search;
} | null) } | null)
| ({ | ({
relationTo: 'payload-jobs'; relationTo: 'payload-jobs';
value: string | PayloadJob; value: number | PayloadJob;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: string | User; value: number | User;
}; };
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@ -950,10 +990,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences". * via the `definition` "payload-preferences".
*/ */
export interface PayloadPreference { export interface PayloadPreference {
id: string; id: number;
user: { user: {
relationTo: 'users'; relationTo: 'users';
value: string | User; value: number | User;
}; };
key?: string | null; key?: string | null;
value?: value?:
@ -973,7 +1013,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations". * via the `definition` "payload-migrations".
*/ */
export interface PayloadMigration { export interface PayloadMigration {
id: string; id: number;
name?: string | null; name?: string | null;
batch?: number | null; batch?: number | null;
updatedAt: string; updatedAt: string;
@ -984,6 +1024,7 @@ export interface PayloadMigration {
* via the `definition` "pages_select". * via the `definition` "pages_select".
*/ */
export interface PagesSelect<T extends boolean = true> { export interface PagesSelect<T extends boolean = true> {
tenant?: T;
title?: T; title?: T;
hero?: hero?:
| T | T
@ -1263,7 +1304,16 @@ export interface CategoriesSelect<T extends boolean = true> {
* via the `definition` "users_select". * via the `definition` "users_select".
*/ */
export interface UsersSelect<T extends boolean = true> { export interface UsersSelect<T extends boolean = true> {
name?: T; roles?: T;
username?: T;
accessLevel?: T;
tenants?:
| T
| {
tenant?: T;
roles?: T;
id?: T;
};
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
email?: T; email?: T;
@ -1274,6 +1324,18 @@ export interface UsersSelect<T extends boolean = true> {
loginAttempts?: T; loginAttempts?: T;
lockUntil?: T; lockUntil?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tenants_select".
*/
export interface TenantsSelect<T extends boolean = true> {
name?: T;
domain?: T;
slug?: T;
allowPublicRead?: T;
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "redirects_select". * via the `definition` "redirects_select".
@ -1355,6 +1417,7 @@ export interface FormsSelect<T extends boolean = true> {
label?: T; label?: T;
width?: T; width?: T;
defaultValue?: T; defaultValue?: T;
placeholder?: T;
options?: options?:
| T | T
| { | {
@ -1532,7 +1595,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
* via the `definition` "header". * via the `definition` "header".
*/ */
export interface Header { export interface Header {
id: string; id: number;
navItems?: navItems?:
| { | {
link: { link: {
@ -1541,11 +1604,11 @@ export interface Header {
reference?: reference?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@ -1561,7 +1624,7 @@ export interface Header {
* via the `definition` "footer". * via the `definition` "footer".
*/ */
export interface Footer { export interface Footer {
id: string; id: number;
navItems?: navItems?:
| { | {
link: { link: {
@ -1570,11 +1633,11 @@ export interface Footer {
reference?: reference?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null); } | null);
url?: string | null; url?: string | null;
label: string; label: string;
@ -1642,14 +1705,14 @@ export interface TaskSchedulePublish {
doc?: doc?:
| ({ | ({
relationTo: 'pages'; relationTo: 'pages';
value: string | Page; value: number | Page;
} | null) } | null)
| ({ | ({
relationTo: 'posts'; relationTo: 'posts';
value: string | Post; value: number | Post;
} | null); } | null);
global?: string | null; global?: string | null;
user?: (string | null) | User; user?: (number | null) | User;
}; };
output?: unknown; output?: unknown;
} }

View File

@ -16,6 +16,7 @@ import { Header } from './Header/config'
import { plugins } from './plugins' import { plugins } from './plugins'
import { defaultLexical } from '@/fields/defaultLexical' import { defaultLexical } from '@/fields/defaultLexical'
import { getServerSideURL } from './utilities/getURL' import { getServerSideURL } from './utilities/getURL'
import { Tenants } from './collections/Tenants'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
@ -64,7 +65,7 @@ export default buildConfig({
connectionString: process.env.DATABASE_URI || '', connectionString: process.env.DATABASE_URI || '',
}, },
}), }),
collections: [Pages, Posts, Media, Categories, Users], collections: [Pages, Posts, Media, Categories, Users, Tenants],
cors: [getServerSideURL()].filter(Boolean), cors: [getServerSideURL()].filter(Boolean),
globals: [Header, Footer], globals: [Header, Footer],
plugins: [ plugins: [

View File

@ -1,4 +1,5 @@
import { payloadCloudPlugin } from '@payloadcms/payload-cloud' import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
import { formBuilderPlugin } from '@payloadcms/plugin-form-builder' import { formBuilderPlugin } from '@payloadcms/plugin-form-builder'
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs' import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
import { redirectsPlugin } from '@payloadcms/plugin-redirects' import { redirectsPlugin } from '@payloadcms/plugin-redirects'
@ -11,8 +12,10 @@ import { FixedToolbarFeature, HeadingFeature, lexicalEditor } from '@payloadcms/
import { searchFields } from '@/search/fieldOverrides' import { searchFields } from '@/search/fieldOverrides'
import { beforeSyncWithSearch } from '@/search/beforeSync' import { beforeSyncWithSearch } from '@/search/beforeSync'
import { Page, Post } from '@/payload-types' import { Config, Page, Post, User } from '@/payload-types'
import { getServerSideURL } from '@/utilities/getURL' import { getServerSideURL } from '@/utilities/getURL'
import { UserAccessLevel } from '@/collections/Users'
import { isSuperAdmin } from '@/access/admin'
const generateTitle: GenerateTitle<Post | Page> = ({ doc }) => { const generateTitle: GenerateTitle<Post | Page> = ({ doc }) => {
return doc?.title ? `${doc.title} | Payload Website Template` : 'Payload Website Template' return doc?.title ? `${doc.title} | Payload Website Template` : 'Payload Website Template'
@ -91,4 +94,78 @@ export const plugins: Plugin[] = [
}, },
}), }),
payloadCloudPlugin(), payloadCloudPlugin(),
multiTenantPlugin<Config>({
debug: true,
enabled: true,
collections: {
pages: {
useBaseListFilter: true,
useTenantAccess: true,
},
},
tenantsArrayField: {
includeDefaultField: false,
},
userHasAccessToAllTenants: (user: User) => (user.accessLevel || 0) >= UserAccessLevel.SUPER_ADMIN,
tenantField: {
access: {
read: () => true,
update: (data) => {
return true
// const { req, doc } = data
//
// if (isSuperAdmin({ req })) return true
//
// if (!req.user) return false
//
// const userTentants = req.user.tenants || []
// if (userTentants.includes(doc.tenant)) return true
//
// return false
},
create: () => true,
},
},
}),
] ]
//mport type { CollectionConfig } from 'payload'
//
//mport { authenticated } from '../../access/authenticated'
//
//xport const Exercises: CollectionConfig = {
// slug: 'exercises',
// access: {
// create: authenticated,
// delete: authenticated,
// read: authenticated,
// update: authenticated,
// },
// admin: {
// useAsTitle: 'name',
// },
// fields: [
// {
// name: 'name',
// type: 'text',
// required: true,
// unique: true,
// },
// {
// name: 'instuctions',
// type: 'text',
// },
//
// {
// name: 'images',
// type: 'relationship',
// relationTo: 'media',
// hasMany: true,
// },
// // add these relationships
// // categories - strength, cardio etc
// // muscle group
// // exercise Type - Bodyweight, dumbells etc
// ],
//