diff --git a/package-lock.json b/package-lock.json index d6ed8d7..5b79c44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@payloadcms/next": "3.33.0", "@payloadcms/payload-cloud": "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-redirects": "3.33.0", "@payloadcms/plugin-search": "3.33.0", @@ -3769,6 +3770,17 @@ "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": { "version": "3.33.0", "resolved": "https://registry.npmjs.org/@payloadcms/plugin-nested-docs/-/plugin-nested-docs-3.33.0.tgz", diff --git a/package.json b/package.json index 3f37470..d1891f1 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,12 @@ }, "dependencies": { "@payloadcms/admin-bar": "3.33.0", + "@payloadcms/db-postgres": "3.33.0", "@payloadcms/live-preview-react": "3.33.0", "@payloadcms/next": "3.33.0", "@payloadcms/payload-cloud": "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-redirects": "3.33.0", "@payloadcms/plugin-search": "3.33.0", @@ -49,8 +51,7 @@ "react-hook-form": "7.45.4", "sharp": "0.32.6", "tailwind-merge": "^2.3.0", - "tailwindcss-animate": "^1.0.7", - "@payloadcms/db-postgres": "3.33.0" + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/src/access/admin.ts b/src/access/admin.ts new file mode 100644 index 0000000..d8cf5ba --- /dev/null +++ b/src/access/admin.ts @@ -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 +} diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index bd78e0f..7cec0cf 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -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 { RscEntryLexicalField as RscEntryLexicalField_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 { default as default_1a7510af427896d367a49dbf838d2de6 } from '@/components/BeforeDashboard' 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 = { - '@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell': - RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e, - '@payloadcms/richtext-lexical/rsc#RscEntryLexicalField': - RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e, - '@payloadcms/richtext-lexical/rsc#LexicalDiffComponent': - LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e, - '@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient': - InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - '@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient': - FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - '@payloadcms/richtext-lexical/client#HeadingFeatureClient': - HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - '@payloadcms/richtext-lexical/client#ParagraphFeatureClient': - ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - '@payloadcms/richtext-lexical/client#UnderlineFeatureClient': - UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - '@payloadcms/richtext-lexical/client#BoldFeatureClient': - BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - '@payloadcms/richtext-lexical/client#ItalicFeatureClient': - ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - '@payloadcms/richtext-lexical/client#LinkFeatureClient': - LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - '@payloadcms/plugin-seo/client#OverviewComponent': - OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860, - '@payloadcms/plugin-seo/client#MetaTitleComponent': - MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860, - '@payloadcms/plugin-seo/client#MetaImageComponent': - MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860, - '@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, + "@payloadcms/plugin-multi-tenant/client#TenantField": TenantField_1d0591e3cf4f332c83a86da13a0de59a, + "@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e, + "@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e, + "@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e, + "@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860, + "@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860, + "@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860, + "@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, + "@payloadcms/plugin-multi-tenant/client#TenantSelector": TenantSelector_1d0591e3cf4f332c83a86da13a0de59a, + "@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62 } diff --git a/src/collections/Tenants/index.ts b/src/collections/Tenants/index.ts new file mode 100644 index 0000000..af6a368 --- /dev/null +++ b/src/collections/Tenants/index.ts @@ -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, + }, + ], +} diff --git a/src/collections/Users/index.ts b/src/collections/Users/index.ts index f0555a7..988a2b6 100644 --- a/src/collections/Users/index.ts +++ b/src/collections/Users/index.ts @@ -1,6 +1,36 @@ import type { CollectionConfig } from 'payload' 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 = { slug: 'users', @@ -13,13 +43,45 @@ export const Users: CollectionConfig = { }, admin: { defaultColumns: ['name', 'email'], - useAsTitle: 'name', + useAsTitle: 'username', }, auth: true, 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', + // hooks: { + // beforeValidate: [ensureUniqueUsername], + // }, + index: true, + }, + { + name: 'accessLevel', + type: 'number', + max: UserAccessLevel.FULL_ACCESS, + defaultValue: UserAccessLevel.GUEST, + }, + { + ...defaultTenantArrayField, + admin: { + ...(defaultTenantArrayField?.admin || {}), + position: 'sidebar', + }, }, ], timestamps: true, diff --git a/src/payload-types.ts b/src/payload-types.ts index 1dfe387..bed795d 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -54,6 +54,7 @@ export type SupportedTimezones = | 'Asia/Singapore' | 'Asia/Tokyo' | 'Asia/Seoul' + | 'Australia/Brisbane' | 'Australia/Sydney' | 'Pacific/Guam' | 'Pacific/Noumea' @@ -71,6 +72,7 @@ export interface Config { media: Media; categories: Category; users: User; + tenants: Tenant; redirects: Redirect; forms: Form; 'form-submissions': FormSubmission; @@ -87,6 +89,7 @@ export interface Config { media: MediaSelect | MediaSelect; categories: CategoriesSelect | CategoriesSelect; users: UsersSelect | UsersSelect; + tenants: TenantsSelect | TenantsSelect; redirects: RedirectsSelect | RedirectsSelect; forms: FormsSelect | FormsSelect; 'form-submissions': FormSubmissionsSelect | FormSubmissionsSelect; @@ -97,7 +100,7 @@ export interface Config { 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; }; db: { - defaultIDType: string; + defaultIDType: number; }; globals: { header: Header; @@ -145,7 +148,8 @@ export interface UserAuthOperations { * via the `definition` "pages". */ export interface Page { - id: string; + id: number; + tenant?: (number | null) | Tenant; title: string; hero: { type: 'none' | 'highImpact' | 'mediumImpact' | 'lowImpact'; @@ -172,11 +176,11 @@ export interface Page { reference?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null); url?: string | null; label: string; @@ -188,7 +192,7 @@ export interface Page { id?: string | null; }[] | null; - media?: (string | null) | Media; + media?: (number | null) | Media; }; layout: (CallToActionBlock | ContentBlock | MediaBlock | ArchiveBlock | FormBlock)[]; meta?: { @@ -196,7 +200,7 @@ export interface Page { /** * Maximum upload file size: 12MB. Recommended file size for images is <500KB. */ - image?: (string | null) | Media; + image?: (number | null) | Media; description?: string | null; }; publishedAt?: string | null; @@ -206,14 +210,36 @@ export interface Page { createdAt: string; _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 * via the `definition` "posts". */ export interface Post { - id: string; + id: number; title: string; - heroImage?: (string | null) | Media; + heroImage?: (number | null) | Media; content: { root: { type: string; @@ -229,18 +255,18 @@ export interface Post { }; [k: string]: unknown; }; - relatedPosts?: (string | Post)[] | null; - categories?: (string | Category)[] | null; + relatedPosts?: (number | Post)[] | null; + categories?: (number | Category)[] | null; meta?: { title?: string | null; /** * Maximum upload file size: 12MB. Recommended file size for images is <500KB. */ - image?: (string | null) | Media; + image?: (number | null) | Media; description?: string | null; }; publishedAt?: string | null; - authors?: (string | User)[] | null; + authors?: (number | User)[] | null; populatedAuthors?: | { id?: string | null; @@ -258,7 +284,7 @@ export interface Post { * via the `definition` "media". */ export interface Media { - id: string; + id: number; alt?: string | null; caption?: { root: { @@ -350,14 +376,14 @@ export interface Media { * via the `definition` "categories". */ export interface Category { - id: string; + id: number; title: string; slug?: string | null; slugLock?: boolean | null; - parent?: (string | null) | Category; + parent?: (number | null) | Category; breadcrumbs?: | { - doc?: (string | null) | Category; + doc?: (number | null) | Category; url?: string | null; label?: string | null; id?: string | null; @@ -371,8 +397,17 @@ export interface Category { * via the `definition` "users". */ export interface User { - id: string; - name?: string | null; + id: number; + 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; createdAt: string; email: string; @@ -412,11 +447,11 @@ export interface CallToActionBlock { reference?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null); url?: string | null; label: string; @@ -462,11 +497,11 @@ export interface ContentBlock { reference?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null); url?: string | null; label: string; @@ -487,7 +522,7 @@ export interface ContentBlock { * via the `definition` "MediaBlock". */ export interface MediaBlock { - media: string | Media; + media: number | Media; id?: string | null; blockName?: string | null; blockType: 'mediaBlock'; @@ -514,12 +549,12 @@ export interface ArchiveBlock { } | null; populateBy?: ('collection' | 'selection') | null; relationTo?: 'posts' | null; - categories?: (string | Category)[] | null; + categories?: (number | Category)[] | null; limit?: number | null; selectedDocs?: | { relationTo: 'posts'; - value: string | Post; + value: number | Post; }[] | null; id?: string | null; @@ -531,7 +566,7 @@ export interface ArchiveBlock { * via the `definition` "FormBlock". */ export interface FormBlock { - form: string | Form; + form: number | Form; enableIntro?: boolean | null; introContent?: { root: { @@ -557,7 +592,7 @@ export interface FormBlock { * via the `definition` "forms". */ export interface Form { - id: string; + id: number; title: string; fields?: | ( @@ -624,6 +659,7 @@ export interface Form { label?: string | null; width?: number | null; defaultValue?: string | null; + placeholder?: string | null; options?: | { label: string; @@ -730,7 +766,7 @@ export interface Form { * via the `definition` "redirects". */ export interface Redirect { - id: string; + id: number; /** * You will need to rebuild the website when changing this field. */ @@ -740,11 +776,11 @@ export interface Redirect { reference?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null); url?: string | null; }; @@ -756,8 +792,8 @@ export interface Redirect { * via the `definition` "form-submissions". */ export interface FormSubmission { - id: string; - form: string | Form; + id: number; + form: number | Form; submissionData?: | { field: string; @@ -775,18 +811,18 @@ export interface FormSubmission { * via the `definition` "search". */ export interface Search { - id: string; + id: number; title?: string | null; priority?: number | null; doc: { relationTo: 'posts'; - value: string | Post; + value: number | Post; }; slug?: string | null; meta?: { title?: string | null; description?: string | null; - image?: (string | null) | Media; + image?: (number | null) | Media; }; categories?: | { @@ -803,7 +839,7 @@ export interface Search { * via the `definition` "payload-jobs". */ export interface PayloadJob { - id: string; + id: number; /** * Input data provided to the job */ @@ -895,52 +931,56 @@ export interface PayloadJob { * via the `definition` "payload-locked-documents". */ export interface PayloadLockedDocument { - id: string; + id: number; document?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null) | ({ relationTo: 'media'; - value: string | Media; + value: number | Media; } | null) | ({ relationTo: 'categories'; - value: string | Category; + value: number | Category; } | null) | ({ relationTo: 'users'; - value: string | User; + value: number | User; + } | null) + | ({ + relationTo: 'tenants'; + value: number | Tenant; } | null) | ({ relationTo: 'redirects'; - value: string | Redirect; + value: number | Redirect; } | null) | ({ relationTo: 'forms'; - value: string | Form; + value: number | Form; } | null) | ({ relationTo: 'form-submissions'; - value: string | FormSubmission; + value: number | FormSubmission; } | null) | ({ relationTo: 'search'; - value: string | Search; + value: number | Search; } | null) | ({ relationTo: 'payload-jobs'; - value: string | PayloadJob; + value: number | PayloadJob; } | null); globalSlug?: string | null; user: { relationTo: 'users'; - value: string | User; + value: number | User; }; updatedAt: string; createdAt: string; @@ -950,10 +990,10 @@ export interface PayloadLockedDocument { * via the `definition` "payload-preferences". */ export interface PayloadPreference { - id: string; + id: number; user: { relationTo: 'users'; - value: string | User; + value: number | User; }; key?: string | null; value?: @@ -973,7 +1013,7 @@ export interface PayloadPreference { * via the `definition` "payload-migrations". */ export interface PayloadMigration { - id: string; + id: number; name?: string | null; batch?: number | null; updatedAt: string; @@ -984,6 +1024,7 @@ export interface PayloadMigration { * via the `definition` "pages_select". */ export interface PagesSelect { + tenant?: T; title?: T; hero?: | T @@ -1263,7 +1304,16 @@ export interface CategoriesSelect { * via the `definition` "users_select". */ export interface UsersSelect { - name?: T; + roles?: T; + username?: T; + accessLevel?: T; + tenants?: + | T + | { + tenant?: T; + roles?: T; + id?: T; + }; updatedAt?: T; createdAt?: T; email?: T; @@ -1274,6 +1324,18 @@ export interface UsersSelect { loginAttempts?: T; lockUntil?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "tenants_select". + */ +export interface TenantsSelect { + name?: T; + domain?: T; + slug?: T; + allowPublicRead?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "redirects_select". @@ -1355,6 +1417,7 @@ export interface FormsSelect { label?: T; width?: T; defaultValue?: T; + placeholder?: T; options?: | T | { @@ -1532,7 +1595,7 @@ export interface PayloadMigrationsSelect { * via the `definition` "header". */ export interface Header { - id: string; + id: number; navItems?: | { link: { @@ -1541,11 +1604,11 @@ export interface Header { reference?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null); url?: string | null; label: string; @@ -1561,7 +1624,7 @@ export interface Header { * via the `definition` "footer". */ export interface Footer { - id: string; + id: number; navItems?: | { link: { @@ -1570,11 +1633,11 @@ export interface Footer { reference?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null); url?: string | null; label: string; @@ -1642,14 +1705,14 @@ export interface TaskSchedulePublish { doc?: | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; } | null) | ({ relationTo: 'posts'; - value: string | Post; + value: number | Post; } | null); global?: string | null; - user?: (string | null) | User; + user?: (number | null) | User; }; output?: unknown; } diff --git a/src/payload.config.ts b/src/payload.config.ts index 1268315..796eae8 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -16,6 +16,7 @@ import { Header } from './Header/config' import { plugins } from './plugins' import { defaultLexical } from '@/fields/defaultLexical' import { getServerSideURL } from './utilities/getURL' +import { Tenants } from './collections/Tenants' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) @@ -64,7 +65,7 @@ export default buildConfig({ connectionString: process.env.DATABASE_URI || '', }, }), - collections: [Pages, Posts, Media, Categories, Users], + collections: [Pages, Posts, Media, Categories, Users, Tenants], cors: [getServerSideURL()].filter(Boolean), globals: [Header, Footer], plugins: [ diff --git a/src/plugins/index.ts b/src/plugins/index.ts index efc8a13..271b2d6 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -1,4 +1,5 @@ import { payloadCloudPlugin } from '@payloadcms/payload-cloud' +import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant' import { formBuilderPlugin } from '@payloadcms/plugin-form-builder' import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs' import { redirectsPlugin } from '@payloadcms/plugin-redirects' @@ -11,8 +12,10 @@ import { FixedToolbarFeature, HeadingFeature, lexicalEditor } from '@payloadcms/ import { searchFields } from '@/search/fieldOverrides' 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 { UserAccessLevel } from '@/collections/Users' +import { isSuperAdmin } from '@/access/admin' const generateTitle: GenerateTitle = ({ doc }) => { return doc?.title ? `${doc.title} | Payload Website Template` : 'Payload Website Template' @@ -91,4 +94,78 @@ export const plugins: Plugin[] = [ }, }), payloadCloudPlugin(), + multiTenantPlugin({ + 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 +// ], +//