feat: users on tenants
This commit is contained in:
@@ -14,7 +14,7 @@ import { filterDocumentsBySelectedTenant } from './list-filters/filterDocumentsB
|
||||
import { filterTenantsBySelectedTenant } from './list-filters/filterTenantsBySelectedTenant.js'
|
||||
import { filterUsersBySelectedTenant } from './list-filters/filterUsersBySelectedTenant.js'
|
||||
import { translations } from './translations/index.js'
|
||||
import { addCollectionAccess } from './utilities/addCollectionAccess.js'
|
||||
import { addCollectionAccess, addUserCollectionAccess } from './utilities/addCollectionAccess.js'
|
||||
import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js'
|
||||
import { combineListFilters } from './utilities/combineListFilters.js'
|
||||
|
||||
@@ -130,10 +130,23 @@ export const multiTenantPlugin =
|
||||
)
|
||||
}
|
||||
|
||||
addCollectionAccess({
|
||||
/**
|
||||
* 🚨 V2
|
||||
*/
|
||||
adminUsersCollection.fields.push({
|
||||
name: 'joinedTenants',
|
||||
type: 'join',
|
||||
admin: {
|
||||
allowCreate: false,
|
||||
},
|
||||
collection: tenantsCollectionSlug,
|
||||
on: 'assignedUsers.user',
|
||||
})
|
||||
|
||||
addUserCollectionAccess({
|
||||
adminUsersSlug: adminUsersCollection.slug,
|
||||
collection: adminUsersCollection,
|
||||
fieldName: `${tenantsArrayFieldName}.${tenantsArrayTenantFieldName}`,
|
||||
fieldName: `joinedTenants`,
|
||||
tenantsArrayFieldName,
|
||||
tenantsArrayTenantFieldName,
|
||||
userHasAccessToAllTenants,
|
||||
@@ -146,11 +159,9 @@ export const multiTenantPlugin =
|
||||
|
||||
adminUsersCollection.admin.baseListFilter = combineListFilters({
|
||||
baseListFilter: adminUsersCollection.admin?.baseListFilter,
|
||||
customFilter: (args) =>
|
||||
filterUsersBySelectedTenant({
|
||||
customFilter: async (args) =>
|
||||
await filterUsersBySelectedTenant({
|
||||
req: args.req,
|
||||
tenantsArrayFieldName,
|
||||
tenantsArrayTenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
}),
|
||||
})
|
||||
@@ -234,6 +245,23 @@ export const multiTenantPlugin =
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 🚨 V2
|
||||
*/
|
||||
collection.fields.push({
|
||||
name: 'assignedUsers',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'user',
|
||||
type: 'relationship',
|
||||
hasMany: false,
|
||||
relationTo: adminUsersCollection.slug,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
/**
|
||||
* Add custom tenant field that watches and dispatches updates to the selector
|
||||
*/
|
||||
|
||||
@@ -1,33 +1,44 @@
|
||||
import type { PayloadRequest, Where } from 'payload'
|
||||
import type { PayloadRequest, TypeWithID, Where } from 'payload'
|
||||
|
||||
import { extractID } from 'payload/shared'
|
||||
|
||||
import { getCollectionIDType } from '../utilities/getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
|
||||
|
||||
type Args = {
|
||||
req: PayloadRequest
|
||||
tenantsArrayFieldName: string
|
||||
tenantsArrayTenantFieldName: string
|
||||
tenantsCollectionSlug: string
|
||||
}
|
||||
/**
|
||||
* Filter the list of users by the selected tenant
|
||||
*/
|
||||
export const filterUsersBySelectedTenant = ({
|
||||
export const filterUsersBySelectedTenant = async ({
|
||||
req,
|
||||
tenantsArrayFieldName,
|
||||
tenantsArrayTenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
}: Args): null | Where => {
|
||||
}: Args): Promise<null | Where> => {
|
||||
const idType = getCollectionIDType({
|
||||
collectionSlug: tenantsCollectionSlug,
|
||||
payload: req.payload,
|
||||
})
|
||||
const selectedTenant = getTenantFromCookie(req.headers, idType)
|
||||
|
||||
if (selectedTenant) {
|
||||
if (req.user && selectedTenant) {
|
||||
const doc = await req.payload.findByID({
|
||||
id: selectedTenant,
|
||||
collection: tenantsCollectionSlug,
|
||||
depth: 0,
|
||||
req,
|
||||
select: {
|
||||
assignedUsers: true,
|
||||
},
|
||||
user: req.user,
|
||||
})
|
||||
|
||||
return {
|
||||
[`${tenantsArrayFieldName}.${tenantsArrayTenantFieldName}`]: {
|
||||
in: [selectedTenant],
|
||||
id: {
|
||||
in: (Array.isArray(doc?.assignedUsers) ? doc.assignedUsers : []).map(
|
||||
({ user: tenantUser }: { user: TypeWithID }) => extractID(tenantUser),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { CollectionConfig } from 'payload'
|
||||
|
||||
import type { MultiTenantPluginConfig } from '../types.js'
|
||||
|
||||
import { withTenantAccess } from './withTenantAccess.js'
|
||||
import { withTenantAccess, withUserTenantAccess } from './withTenantAccess.js'
|
||||
|
||||
type AllAccessKeys<T extends readonly string[]> = T[number] extends keyof Omit<
|
||||
Required<CollectionConfig>['access'],
|
||||
@@ -56,3 +56,28 @@ export const addCollectionAccess = <ConfigType>({
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const addUserCollectionAccess = <ConfigType>({
|
||||
adminUsersSlug,
|
||||
collection,
|
||||
fieldName,
|
||||
tenantsArrayFieldName,
|
||||
tenantsArrayTenantFieldName,
|
||||
userHasAccessToAllTenants,
|
||||
}: Args<ConfigType>): void => {
|
||||
collectionAccessKeys.forEach((key) => {
|
||||
if (!collection.access) {
|
||||
collection.access = {}
|
||||
}
|
||||
collection.access[key] = withUserTenantAccess<ConfigType>({
|
||||
accessFunction: collection.access?.[key],
|
||||
adminUsersSlug,
|
||||
collection,
|
||||
fieldName: key === 'readVersions' ? `version.${fieldName}` : fieldName,
|
||||
operation: key,
|
||||
tenantsArrayFieldName,
|
||||
tenantsArrayTenantFieldName,
|
||||
userHasAccessToAllTenants,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Where } from 'payload'
|
||||
import type { TypeWithID, Where } from 'payload'
|
||||
|
||||
import type { UserWithTenantsField } from '../types.js'
|
||||
|
||||
@@ -22,9 +22,14 @@ export function getTenantAccess({
|
||||
tenantsArrayTenantFieldName,
|
||||
})
|
||||
|
||||
/**
|
||||
* 🚨 V2
|
||||
*/
|
||||
const joinedTenants = user.joinedTenants?.docs.map((doc: TypeWithID) => doc.id) || []
|
||||
|
||||
return {
|
||||
[fieldName]: {
|
||||
in: userAssignedTenantIDs || [],
|
||||
in: joinedTenants || [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@ import type {
|
||||
AllOperations,
|
||||
CollectionConfig,
|
||||
TypedUser,
|
||||
TypeWithID,
|
||||
Where,
|
||||
} from 'payload'
|
||||
|
||||
import { extractID } from 'payload/shared'
|
||||
|
||||
import type { MultiTenantPluginConfig, UserWithTenantsField } from '../types.js'
|
||||
|
||||
import { combineWhereConstraints } from './combineWhereConstraints.js'
|
||||
@@ -62,6 +65,7 @@ export const withTenantAccess =
|
||||
tenantsArrayTenantFieldName,
|
||||
user: args.req.user as UserWithTenantsField,
|
||||
})
|
||||
|
||||
if (collection.slug === args.req.user.collection) {
|
||||
constraints.push({
|
||||
or: [
|
||||
@@ -81,3 +85,81 @@ export const withTenantAccess =
|
||||
|
||||
return accessResult
|
||||
}
|
||||
|
||||
export const withUserTenantAccess =
|
||||
<ConfigType>({
|
||||
accessFunction,
|
||||
adminUsersSlug,
|
||||
collection,
|
||||
fieldName,
|
||||
tenantsArrayFieldName,
|
||||
tenantsArrayTenantFieldName,
|
||||
userHasAccessToAllTenants,
|
||||
}: Args<ConfigType>) =>
|
||||
async (args: AccessArgs): Promise<AccessResult> => {
|
||||
const constraints: Where[] = []
|
||||
const accessFn =
|
||||
typeof accessFunction === 'function'
|
||||
? accessFunction
|
||||
: ({ req }: AccessArgs): AccessResult => Boolean(req.user)
|
||||
const accessResult: AccessResult = await accessFn(args)
|
||||
|
||||
if (accessResult === false) {
|
||||
return false
|
||||
} else if (accessResult && typeof accessResult === 'object') {
|
||||
constraints.push(accessResult)
|
||||
}
|
||||
|
||||
if (
|
||||
args.req.user &&
|
||||
args.req.user.collection === adminUsersSlug &&
|
||||
!userHasAccessToAllTenants(
|
||||
args.req.user as ConfigType extends { user: unknown } ? ConfigType['user'] : TypedUser,
|
||||
)
|
||||
) {
|
||||
const tenantConstraint = getTenantAccess({
|
||||
fieldName,
|
||||
tenantsArrayFieldName,
|
||||
tenantsArrayTenantFieldName,
|
||||
user: args.req.user as UserWithTenantsField,
|
||||
})
|
||||
|
||||
const joinedTenants = args.req.user.joinedTenants?.docs.map((doc: TypeWithID) => doc.id) || []
|
||||
|
||||
const tenantUserIDs = await args.req.payload.find({
|
||||
collection: 'tenants',
|
||||
depth: 0,
|
||||
limit: 0,
|
||||
req: args.req,
|
||||
select: {
|
||||
assignedUsers: true,
|
||||
},
|
||||
user: args.req.user,
|
||||
where: {
|
||||
id: {
|
||||
in: joinedTenants,
|
||||
},
|
||||
},
|
||||
})
|
||||
const userIDs = new Set<number | string>([args.req.user.id])
|
||||
tenantUserIDs.docs.forEach((doc: any) => {
|
||||
doc.assignedUsers.forEach(({ user: tenantUser }: { user: TypeWithID }) => {
|
||||
userIDs.add(extractID(tenantUser))
|
||||
})
|
||||
})
|
||||
|
||||
if (collection.slug === args.req.user.collection) {
|
||||
constraints.push({
|
||||
id: {
|
||||
in: Array.from(userIDs),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
constraints.push(tenantConstraint)
|
||||
}
|
||||
|
||||
return combineWhereConstraints(constraints)
|
||||
}
|
||||
|
||||
return accessResult
|
||||
}
|
||||
|
||||
@@ -24,11 +24,5 @@ export const Tenants: CollectionConfig = {
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'join',
|
||||
name: 'users',
|
||||
collection: 'users',
|
||||
on: 'tenants.tenant',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -7,8 +7,12 @@ export const credentials = {
|
||||
email: 'jane@blue-dog.com',
|
||||
password: 'test',
|
||||
},
|
||||
steelCat: {
|
||||
email: 'huel@steel-cat.com',
|
||||
password: 'test',
|
||||
},
|
||||
owner: {
|
||||
email: 'owner@anchorAndBlueDog.com',
|
||||
email: 'owner@AnchorAndBlueDog.com',
|
||||
password: 'test',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ export interface Config {
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {
|
||||
tenants: {
|
||||
users: 'users';
|
||||
users: {
|
||||
joinedTenants: 'tenants';
|
||||
};
|
||||
};
|
||||
collectionsSelect: {
|
||||
@@ -129,11 +129,12 @@ export interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
domain: string;
|
||||
users?: {
|
||||
docs?: (string | User)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
assignedUsers?:
|
||||
| {
|
||||
user: string | User;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -150,6 +151,11 @@ export interface User {
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
joinedTenants?: {
|
||||
docs?: (string | Tenant)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -273,7 +279,12 @@ export interface PayloadMigration {
|
||||
export interface TenantsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
domain?: T;
|
||||
users?: T;
|
||||
assignedUsers?:
|
||||
| T
|
||||
| {
|
||||
user?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
@@ -289,6 +300,7 @@ export interface UsersSelect<T extends boolean = true> {
|
||||
tenant?: T;
|
||||
id?: T;
|
||||
};
|
||||
joinedTenants?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
|
||||
@@ -4,12 +4,53 @@ import { credentials } from '../credentials.js'
|
||||
import { menuItemsSlug, menuSlug, tenantsSlug, usersSlug } from '../shared.js'
|
||||
|
||||
export const seed: Config['onInit'] = async (payload) => {
|
||||
// create users
|
||||
await payload.create({
|
||||
collection: usersSlug,
|
||||
data: {
|
||||
...credentials.admin,
|
||||
roles: ['admin'],
|
||||
},
|
||||
})
|
||||
|
||||
const blueDogUser = await payload.create({
|
||||
collection: usersSlug,
|
||||
data: {
|
||||
...credentials.blueDog,
|
||||
roles: ['user'],
|
||||
},
|
||||
})
|
||||
|
||||
const blueDogAndAnchorUser = await payload.create({
|
||||
collection: usersSlug,
|
||||
data: {
|
||||
...credentials.owner,
|
||||
roles: ['user'],
|
||||
},
|
||||
})
|
||||
|
||||
const steelCatUser = await payload.create({
|
||||
collection: usersSlug,
|
||||
data: {
|
||||
...credentials.steelCat,
|
||||
roles: ['user'],
|
||||
},
|
||||
})
|
||||
|
||||
// create tenants
|
||||
const blueDogTenant = await payload.create({
|
||||
collection: tenantsSlug,
|
||||
data: {
|
||||
name: 'Blue Dog',
|
||||
domain: 'bluedog.com',
|
||||
assignedUsers: [
|
||||
{
|
||||
user: blueDogUser.id,
|
||||
},
|
||||
{
|
||||
user: blueDogAndAnchorUser.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const steelCatTenant = await payload.create({
|
||||
@@ -17,6 +58,11 @@ export const seed: Config['onInit'] = async (payload) => {
|
||||
data: {
|
||||
name: 'Steel Cat',
|
||||
domain: 'steelcat.com',
|
||||
assignedUsers: [
|
||||
{
|
||||
user: steelCatUser.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const anchorBarTenant = await payload.create({
|
||||
@@ -24,6 +70,11 @@ export const seed: Config['onInit'] = async (payload) => {
|
||||
data: {
|
||||
name: 'Anchor Bar',
|
||||
domain: 'anchorbar.com',
|
||||
assignedUsers: [
|
||||
{
|
||||
user: blueDogAndAnchorUser.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -73,44 +124,6 @@ export const seed: Config['onInit'] = async (payload) => {
|
||||
},
|
||||
})
|
||||
|
||||
// create users
|
||||
await payload.create({
|
||||
collection: usersSlug,
|
||||
data: {
|
||||
...credentials.admin,
|
||||
roles: ['admin'],
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: usersSlug,
|
||||
data: {
|
||||
...credentials.blueDog,
|
||||
roles: ['user'],
|
||||
tenants: [
|
||||
{
|
||||
tenant: blueDogTenant.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: usersSlug,
|
||||
data: {
|
||||
...credentials.owner,
|
||||
roles: ['user'],
|
||||
tenants: [
|
||||
{
|
||||
tenant: anchorBarTenant.id,
|
||||
},
|
||||
{
|
||||
tenant: blueDogTenant.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// create menus
|
||||
await payload.create({
|
||||
collection: menuSlug,
|
||||
@@ -128,18 +141,4 @@ export const seed: Config['onInit'] = async (payload) => {
|
||||
tenant: steelCatTenant.id,
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: usersSlug,
|
||||
data: {
|
||||
email: 'huel@steel-cat.com',
|
||||
password: 'test',
|
||||
roles: ['user'],
|
||||
tenants: [
|
||||
{
|
||||
tenant: steelCatTenant.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user