joas8211 / payload-tenancy Goto Github PK
View Code? Open in Web Editor NEWMulti-tenancy plugin for Payload CMS
License: MIT License
Multi-tenancy plugin for Payload CMS
License: MIT License
The readme is great and figured that I should mention that I had to go back to discord to find the link to the package on NPM https://www.npmjs.com/package/payload-tenancy
. I know it's a small detail but could be nice to include it with the install command in the readme directly.
Willing to open a quick PR if it's helpful
This isn't an issue, more so a question (sorry I didn't see a discussion board to post to) - How do I create custom domains for tenants, eg. tenant1.app.com, tenant2.app.com?
I've set up a simple project with the following config:
export default buildConfig({
admin: {
user: Users.slug,
bundler: webpackBundler(),
},
editor: lexicalEditor({}),
collections: [
Media,
Tenants,
Users
],
typescript: {
outputFile: path.resolve(__dirname, 'payload-types.ts'),
},
graphQL: {
schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
},
globals: [
Footer
],
plugins: [
tenancy({
isolationStrategy: "domain"
}),
],
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URI,
},
}),
})
And here's my Footer
global:
export const Footer: GlobalConfig = {
slug: 'footer',
access: {
update: isAdmin,
},
fields: [
{
name: 'tenant',
type: 'relationship',
relationTo: 'tenants',
hidden: true,
},
{
name: 'columns',
type: 'array',
admin: {
description: "The columns for your footer.",
components: {
RowLabel: ({data, index}: any) => data?.columnTitle || `Column ${index}`
}
},
fields: [
{
name: "columnTitle",
label: "Column Title",
type: "text",
required: true,
},
],
},
],
}
But I get the following error:
[11:35:46] ERROR (payload): error: invalid reference to FROM-clause entry for table "tenants"
at /home/bradleyk/IdeaProjects/retrobie/retrobie-backend/node_modules/pg/lib/client.js:526:17
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at /home/bradleyk/IdeaProjects/retrobie/retrobie-backend/node_modules/src/node-postgres/session.ts:64:19
at find (/home/bradleyk/IdeaProjects/retrobie/retrobie-backend/node_modules/@payloadcms/db-postgres/src/find/findMany.ts:97:28)
at Object.findGlobal (/home/bradleyk/IdeaProjects/retrobie/retrobie-backend/node_modules/@payloadcms/db-postgres/src/findGlobal.ts:18:7)
at findOne (/home/bradleyk/IdeaProjects/retrobie/retrobie-backend/node_modules/payload/src/globals/operations/findOne.ts:53:15)
at handler (/home/bradleyk/IdeaProjects/retrobie/retrobie-backend/node_modules/payload/src/globals/requestHandlers/findOne.ts:28:22)
Any ideas?
When a user is created in a specific tenant, it cannot be created with the same email on another tenant. For backend users (user editing via the payload admin panel) this is not a problem, but it is a problem for front-end users (customers that log into an ecommerce site).
As I see it we can fix this in a couple of ways:
How I see option 3 would start by adding a config option to this plugin which allows us to choose which auth collection should be used to set the tenant and filter the docs in each collection. So:
plugins: [tenancy({ authCollection: "users" })],
Then we can create a seperate auth collection in payload for the frontend, which we call customers
in this case.
This plugin would then need to create a collection named mytenant-customers
and route the api request to the correct ones.
Pro: seems like easiest of the 3 to make
Con: Not in line with the nature of this plugin, which is filtering the same collection, not creating new ones
Would love to hear what you think!
Hello! Thank you for making this very useful plugin!
Some use cases such as mine only need 1-2 layers of tenancy.
Restricting this to a certain depth also patches up the "infinite subtenants" possibility.
With that, I suggest adding a depth setting for creating tenants.
Steps to reproduce:
import { GlobalConfig } from "payload/types";
export const Global: GlobalConfig = {
slug: "Test",
label: "Test",
fields: [
{
name: "name",
type: "text",
localized: true,
}
]
}
I am able to get the desired behaviour when I disable the tenancy plugin, for this issue I'm using my blank payload template (not my working project). This only appears to be happening on globals and not collections.
Demo:
I have succeeded in creating my user and root tenant however, when trying to create further tenants I am receiving a unauthorized error on the parent
field:
ERROR (payload): ValidationError: The following field is invalid: parent
at beforeChange (/home/node/app/node_modules/payload/src/fields/hooks/beforeChange/index.ts:56:11)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at create (/home/node/app/node_modules/payload/src/collections/operations/create.ts:197:31)
at createHandler (/home/node/app/node_modules/payload/src/collections/requestHandlers/create.ts:26:17)
Dependencies:
{
"dependencies": {
"@aws-sdk/client-s3": "^3.427.0",
"@aws-sdk/lib-storage": "^3.427.0",
"@payloadcms/bundler-webpack": "^1.0.3",
"@payloadcms/db-mongodb": "^1.0.3",
"@payloadcms/plugin-cloud": "^0.0.10",
"@payloadcms/plugin-cloud-storage": "^1.0.19",
"@payloadcms/richtext-lexical": "^0.1.5",
"cross-env": "^7.0.3",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"payload": "^2.0.13",
"payload-tenancy": "^2.0.0"
},
It is also a required field so I can't create any additional tenants.
Any help appreciated!
Screencap
Admin panel front-end won't render for tenants that have slug with special characters. This happens when percent-encoding is used for the path.
For the new globals feature you added the following explanation in the docs:
To operate on isolated globals using Local API, you must pass user object with a tenant so that the correct document is accessed.
const globalDocument = await payload.findGlobal({
slug: "settings",
user: { tenant: someTenantOrId },
});
For regular collection, is adding user: { tenant: someTenantOrId }
also the way to go?
Currently fields added by this plugin cannot be modified by the project using the plugin. Only plugins that are applied after this plugin can currently modify the fields.
It should be to be possible to define fields in project's config by using same names as fields in this plugin is using and override properties that way.
// Plugin adds following field:
const pluginTenantSlugField = {
type: "text",
name: "slug",
unique: true,
required: true,
};
// Field can be extended by defining the field in project's config:
const projectConfig = {
collections: [
{
slug: "tenants",
fields: [
{
type: "text",
name: "slug",
beforeValidate: ({ value }) => {
if (!value || typeof value !== "string") {
return "";
}
// Format slug.
return value
.trim()
.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/--+/g, "-")
.replace(/^-+|-+$/g, "");
},
},
],
},
],
};
// Resulting field uses the project's field if possible and applies missing
// properties from plugin's field. Or uses the plugin's field if project does
// not have a field with the same name.
const projectTenantSlugField = tenantCollection.fields.find(
(field) => field.name === "slug"
);
const resultingField = { ...pluginTenantSlugField, ...projectTenantSlugField };
The above way of merging works for this case but it might be neccessary to implement a merge function that handles nested objects.
Hey I'm thinking about using this with the newly released payload 2.0 and postgres adapter is it compatible with either of those?
This could be done by implementing an option to specify the label for tenant in the options. You could say that tenant = "Organization"
and it would change all the field labels. Or maybe something like that was suggested in #7 (comment), so that it would find the tenant label from the existing config.
I have a use case as follows:
I have a single global tenant (Let's call this HQ).
I have multiple "Groups" (sub-tenants of HQ).
Each group has one or more territories under them (sub-tenants of the group).
The Groups themselves don't have a website, but each territory does. The groups are here only for access control to their territories.
Some users need to be at the "Group" level to log into any individual territory within the group, but should not be able to login to the group itself.
Some users need to be at the group level so that they can create new territories within the group.
Some users are specific to a territory - this already works.
Also, the group does not need any other collections than User and Tenant... Is there a way to hide my resource collections and globals from the "Group" level so that they can only see their child tenants and users?
Any help/guidance would be appreciated here.
After installation, it's required to create the root tenant. Accesses are configured to restrict access to only tenant collection if there's no tenants yet. But after the root tenant is created, admin panel still doesn't show other collections since the accesses has not been refreshed after submitting the form.
I had a simpler multi tenant application and so far this has been much more seamless, so thank you!
I had two questions though, let me know if it's possible to support.
I am using a custom collection as the tenant ex: tenantCollection: Business.slug
and noticed that the field still says tenant instead of the business name. I was wondering if it was possible to extend the configuration so that the relationship here could use another label? Also making it read only unless you are a user from the parent tenant could be handy too
Also is it possible to hide this field from the table view, when I visit it as an admin user or user from a parent tenant it shows up as undefined
When I view it was a user of the tenant it says no parent
hiding the column from this view would be great!
I would like to use this plugin as a multisite (blogging) solution. Our company manages 3 websites and I would like users to be able to switch environments (maybe through their profile), so they will only see the content for the website they are working on.
What I'm confused about is that the second tenant needs to have a parent tenant. Isn't it possible to have tenants act as websites that have nothing to do with each other?
Also, when my user is connected to the root tenant and then switch to a child tenant, it works, but I can never switch back to any other tenant.
Some advice would be appreciated!
Thanks
Hi,
The search limit in PayloadCMS is set to 10 by default. Because of this, if there are more than 10 child tenants, the list only shows the first 10. Can I change the settings to increase the limit above 10?
When a user opens any collection in the admin view (e.g. comments, posts, pages, etc), payload gives this error:
QueryError: The following path cannot be queried: version.tenant
at validateQueryPaths (/home/node/app/node_modules/payload/src/database/queryValidation/validateQueryPaths.ts:91:13)
using:
"payload": "^2.11.1",
"payload-tenancy": "^2.1.1",
After some investigation, this seems to only happen on some collections, I'm using the payload seed project, and in collections that don't load there is this line:
versions: {
drafts: true,
},
npm install payload-tenancy
npm ERR! code EUNSUPPORTEDPROTOCOL
npm ERR! Unsupported URL Type "workspace:": workspace:*
Hi!
I'm having trouble with making REST requests with API requests. I'm using path strategy.
API key usage is enabled on the Users collection:
const Users: CollectionConfig = {
// ...
auth: {
useAPIKey: true,
},
admin: {
useAsTitle: "email",
},
// ...
};
When making REST requests like
curl --location 'http://localhost:3000/[TENANT_SLUG]/api/pages/[PAGE_ID]' \
--header 'Authorization: pages API-Key [API KEY GENERATED FOR THE USER ]'
The request fails with
payload-cms-payload-1 | [13:43:35] ERROR (payload): Forbidden: You are not allowed to perform this action.
payload-cms-payload-1 | at executeAccess (/home/node/app/node_modules/payload/src/auth/executeAccess.ts:10:43)
payload-cms-payload-1 | at processTicksAndRejections (node:internal/process/task_queues:95:5)
payload-cms-payload-1 | at async find (/home/node/app/node_modules/payload/src/collections/operations/find.ts:84:22)
payload-cms-payload-1 | at async findHandler (/home/node/app/node_modules/payload/src/collections/requestHandlers/find.ts:30:20)
On the other hand, if I enable API keys of the pages collection and use the key generated that way, the request succeeds.
Is there something I'm missing?
Thanks in advance
When installed package it brakes the Admin UI. Also tested several times on a blank Payload CMS project just to install payload-tenancy (npm install payload-tenancy) it brakes the admin UI, and when uninstalled the plugin, it is still broken.
React error:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
Tenant field should be hidden from the setup form. There's no tenants yet at setup, so no value can be set. Clicking the field shows the loading animation indefinitely. Tenant is created after creating the first user.
This cannot be achieved by restricting access, because access rights are not taken into account in setup. There might not be a solution for this at current point in time without a fix / feature for Payload.
Are you going to upgrade this to Payload 3.0 when that is released?
I have a fresh create-payload-app and ran yarn add payload-tenancy, and added basic Tenants Collection (as in docs), but added isolation strategy to 'path' and after it the payload admin panel cannot be loaded anymore. If tenant isolation is changed to domain or user, then it works all properly.
I noticed that a slug can be saved in a invalid format for example my slug
, ideally this would convert to my-slug
for the user.
In other payload projects i've used this utils in a beforeValidate
hook, what are your thoughts of including it into the project or a similar solution?
function formatSlug({ value }) {
if (!value || typeof value !== "string") {
return "";
}
return value
.trim()
.toLowerCase()
.replace(/[^\w\s-]/g, "") // Remove non-word characters
.replace(/\s+/g, "-") // Replace spaces with hyphens
.replace(/--+/g, "-") // Replace multiple hyphens with a single hyphen
.replace(/^-+|-+$/g, ""); // Remove leading/trailing hyphens
}
Alternatively is it possible to extend your tenancy collection to pass this in myself for the field?
I was wondering if this was supported by default, behind an option or if possible to solve through writing custom hooks for access control.
I have three collections (Users, Media, Businesses)
My admin user is related to main business tenancy (let's call it A)
A user joins under a new business, as a child to my base tenancy (let's call that tenancy B)
User of tenancy B adds a image to the media file
My admin user associated to the base tenancy (A) can't see that image in the media collection
is that suppose to happen? Because when I go into the User and Business collections I can see all the child tenant data
Thank you for any clarification ππ½
Hi there,
I've added this package to my payload, and configured it as specified, however I'm running into an error when logging in to my account.
If I try to create the account programmatically, I get this error on log-in. If I try to create the account via the payload CMS first-user sign up, I get the same error.
Error:
TypeError: Cannot read properties of undefined (reading 'id')
at eval (webpack-internal:///(api)/./node_modules/payload-tenancy/dist/utils/defaultAccess.js:170:60)
at step (webpack-internal:///(api)/./node_modules/payload-tenancy/dist/utils/defaultAccess.js:107:23)
at Object.eval [as next] (webpack-internal:///(api)/./node_modules/payload-tenancy/dist/utils/defaultAccess.js:48:20)
at fulfilled (webpack-internal:///(api)/./node_modules/payload-tenancy/dist/utils/defaultAccess.js:11:32)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
Payload.config
collections: [Users, Tenants, Pages, Posts, Category, Media],
admin: {
user: Users.slug,
},
...
tenancy({ isolationStrategy: "domain" }),
...
Users Collection:
export const Users: CollectionConfig = {
slug: "users",
auth: true,
admin: {
useAsTitle: "email",
},
fields: [
{
name: "firstName",
type: "text",
},
{
name: "lastName",
type: "text",
},
],
};
Tenants Collection:
export const Tenants: CollectionConfig = {
slug: "tenants",
admin: {
useAsTitle: "name",
},
fields: [
{
type: "text",
name: "name",
label: "Name",
required: true,
},
],
};
Seed Script:
const myTenant = await payload.create({
collection: "tenants",
data: {
slug: "my-company",
domains: [{ domain: "french-gossip.com" }],
name: "French Gossip",
},
});
await payload.create({
collection: "users",
data: {
firstName: "Nachi",
lastName: "Robbins",
password: "password",
email: "[email protected]",
roles: ["super-admin"],
tenant: myTenant.id,
},
});
Any suggestions would be super appreciated!
First off, thank you for creating this!
The plugin works seamlessly for collections, however, it seems that globals are shared between all tenants.
I have two users and three tenants and no matter which tenant or user I use, the globals update for all of them.
I am not sure if this is by design or an issue with my setup.
Please let me know if there are other details I can provide.
Hi!
I create new "blank" instance of payload with this plugin. Also create shared global "Settings".
In admin panel user can create shared global "Settings" record, successfully save it to DB(Mongo), but than user cannot change value of this global. In dev panel in browser I see that new values POST-ed to server, but server response old values.
Hello,
Thank you for building this π
I'm using the Cloud Upload plugin to keep my media in a storage bucket. I'm able to upload while logged into a tenant (tested both path & user strats) and the file gets uploaded. I'm not however able to GET the image via any combination of paths to the asset. While uploading, I get the following error:
[18:14:30] ERROR (payload): TypeError: Cannot read properties of undefined (reading 'skipTenancyUploadAfterReadHook')
at /Volumes/GenericUser/GenericUser/payload-cms/node_modules/payload-tenancy/dist/hooks/upload.js:67:41
at step (/Volumes/GenericUser/GenericUser/payload-cms/node_modules/payload-tenancy/dist/hooks/upload.js:44:23)
at Object.next (/Volumes/GenericUser/GenericUser/payload-cms/node_modules/payload-tenancy/dist/hooks/upload.js:25:53)
at /Volumes/GenericUser/GenericUser/payload-cms/node_modules/payload-tenancy/dist/hooks/upload.js:19:71
at new Promise (<anonymous>)
at __awaiter (/Volumes/GenericUser/GenericUser/payload-cms/node_modules/payload-tenancy/dist/hooks/upload.js:15:12)
at /Volumes/GenericUser/GenericUser/payload-cms/node_modules/payload-tenancy/dist/hooks/upload.js:61:16
at /Volumes/GenericUser/GenericUser/payload-cms/node_modules/payload/src/collections/operations/find.ts:223:24
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at async /Volumes/GenericUser/GenericUser/payload-cms/node_modules/payload/src/collections/operations/find.ts:220:7
The media upload gets mapped to a correct looking URL:
User Strategy
http://localhost:3000/media/images/random-image.jpg
Path Strategy
http://localhost:3000/root/media/images/random-image.jpg
But both result in a failure to retrieve the media asset:
Cannot GET /media/images/random-image.jpg
When querying resource collections, the tenant
field is not returned, making it impossible for the frontend to filter documents according to the tenant.
For example, here is the "pages" resource.
Even querying via GraphQL leads to the same result - tenant is always null.
And, since the tenant
field doesn't exist, the frontend can't filter by tenant id
, and users can see each other's documents.
Adding hidden: false
to the CollectionConfig
seems to help, but only when querying for a single page:
But when querying all the pages, it remains null:
However, it works just fine for the user collection:
Any clues what the issue might be?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.