feat: make auth based off user roles and tenant access, and tenant roles
This commit is contained in:
parent
eb2737fd5e
commit
723dda191b
10
src/access/isSuperAdmin.ts
Normal file
10
src/access/isSuperAdmin.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { Access } from 'payload'
|
||||||
|
import { User } from '../payload-types'
|
||||||
|
|
||||||
|
export const isSuperAdminAccess: Access = ({ req }): boolean => {
|
||||||
|
return isSuperAdmin(req.user)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isSuperAdmin = (user: User | null): boolean => {
|
||||||
|
return Boolean(user?.roles?.includes('super-admin'))
|
||||||
|
}
|
||||||
39
src/collections/Tenants/access/byTenant.ts
Normal file
39
src/collections/Tenants/access/byTenant.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import type { Access } from 'payload'
|
||||||
|
|
||||||
|
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||||
|
|
||||||
|
export const filterByTenantRead: Access = (args) => {
|
||||||
|
// Allow public tenants to be read by anyone
|
||||||
|
if (!args.req.user) {
|
||||||
|
return {
|
||||||
|
allowPublicRead: {
|
||||||
|
equals: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canMutateTenant: Access = ({ req }) => {
|
||||||
|
if (!req.user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSuperAdmin(req.user)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: {
|
||||||
|
in:
|
||||||
|
req.user?.tenants
|
||||||
|
?.map(({ roles, tenant }) =>
|
||||||
|
roles?.includes('tenant-admin')
|
||||||
|
? tenant && (typeof tenant === 'string' ? tenant : tenant.id)
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
.filter(Boolean) || [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/collections/Tenants/access/updateAndDelete.ts
Normal file
19
src/collections/Tenants/access/updateAndDelete.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { isSuperAdmin } from '@/access/isSuperAdmin'
|
||||||
|
import { getUserTenantIDs } from '@/utilities/getUserTenantIds'
|
||||||
|
import { Access } from 'payload'
|
||||||
|
|
||||||
|
export const updateAndDeleteAccess: Access = ({ req }) => {
|
||||||
|
if (!req.user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSuperAdmin(req.user)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: {
|
||||||
|
in: getUserTenantIDs(req.user, 'tenant-admin'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +1,15 @@
|
|||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
//import { updateAndDeleteAccess } from './access/updateAndDelete'
|
import { isSuperAdminAccess } from '@/access/isSuperAdmin'
|
||||||
import { isSuperAdmin } from '@/access/admin'
|
import { updateAndDeleteAccess } from './access/updateAndDelete'
|
||||||
|
|
||||||
export const Tenants: CollectionConfig = {
|
export const Tenants: CollectionConfig = {
|
||||||
slug: 'tenants',
|
slug: 'tenants',
|
||||||
access: {
|
access: {
|
||||||
create: isSuperAdmin,
|
create: isSuperAdminAccess,
|
||||||
delete: isSuperAdmin, // change these to the example soon!
|
delete: updateAndDeleteAccess,
|
||||||
read: ({ req }) => true,
|
read: ({ req }) => Boolean(req.user),
|
||||||
update: isSuperAdmin,
|
update: updateAndDeleteAccess,
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
useAsTitle: 'name',
|
useAsTitle: 'name',
|
||||||
|
|||||||
24
src/collections/Users/access/create.ts
Normal file
24
src/collections/Users/access/create.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { Access } from 'payload'
|
||||||
|
|
||||||
|
import type { User } from '../../../payload-types'
|
||||||
|
|
||||||
|
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||||
|
import { getUserTenantIDs } from '@/utilities/getUserTenantIds'
|
||||||
|
|
||||||
|
export const createAccess: Access<User> = ({ req }) => {
|
||||||
|
if (!req.user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSuperAdmin(req.user)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminTenantAccessIDs = getUserTenantIDs(req.user, 'tenant-admin')
|
||||||
|
|
||||||
|
if (adminTenantAccessIDs.length) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
5
src/collections/Users/access/isAccessingSelf.ts
Normal file
5
src/collections/Users/access/isAccessingSelf.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { User } from '@/payload-types'
|
||||||
|
|
||||||
|
export const isAccessingSelf = ({ id, user }: { user?: User; id?: string | number }): boolean => {
|
||||||
|
return user ? Boolean(user.id === id) : false
|
||||||
|
}
|
||||||
56
src/collections/Users/access/read.ts
Normal file
56
src/collections/Users/access/read.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import type { User } from '@/payload-types'
|
||||||
|
import type { Access, Where } from 'payload'
|
||||||
|
import { getTenantFromCookie } from '@payloadcms/plugin-multi-tenant/utilities'
|
||||||
|
|
||||||
|
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||||
|
import { isAccessingSelf } from './isAccessingSelf'
|
||||||
|
import { getCollectionIDType } from '@/utilities/getCollectionIDType'
|
||||||
|
import { getUserTenantIDs } from '@/utilities/getUserTenantIds'
|
||||||
|
|
||||||
|
export const readAccess: Access<User> = ({ req, id }) => {
|
||||||
|
if (!req?.user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAccessingSelf({ id, user: req.user })) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const superAdmin = isSuperAdmin(req.user)
|
||||||
|
const selectedTenant = getTenantFromCookie(
|
||||||
|
req.headers,
|
||||||
|
getCollectionIDType({ payload: req.payload, collectionSlug: 'tenants' }),
|
||||||
|
)
|
||||||
|
const adminTenantAccessIDs = getUserTenantIDs(req.user, 'tenant-admin')
|
||||||
|
|
||||||
|
if (selectedTenant) {
|
||||||
|
// If it's a super admin, or they have access to the tenant ID set in cookie
|
||||||
|
const hasTenantAccess = adminTenantAccessIDs.some((id) => id === selectedTenant)
|
||||||
|
if (superAdmin || hasTenantAccess) {
|
||||||
|
return {
|
||||||
|
'tenants.tenant': {
|
||||||
|
equals: selectedTenant,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (superAdmin) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
or: [
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
equals: req.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'tenants.tenant': {
|
||||||
|
in: adminTenantAccessIDs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as Where
|
||||||
|
}
|
||||||
31
src/collections/Users/access/updateAndDelete.ts
Normal file
31
src/collections/Users/access/updateAndDelete.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type { Access } from 'payload'
|
||||||
|
|
||||||
|
import { isSuperAdmin } from '@/access/isSuperAdmin'
|
||||||
|
import { isAccessingSelf } from './isAccessingSelf'
|
||||||
|
import { getUserTenantIDs } from '@/utilities/getUserTenantIds'
|
||||||
|
|
||||||
|
export const updateAndDeleteAccess: Access = ({ req, id }) => {
|
||||||
|
const { user } = req
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSuperAdmin(user) || isAccessingSelf({ user, id })) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrains update and delete access to users that belong
|
||||||
|
* to the same tenant as the tenant-admin making the request
|
||||||
|
*
|
||||||
|
* You may want to take this a step further with a beforeChange
|
||||||
|
* hook to ensure that the a tenant-admin can only remove users
|
||||||
|
* from their own tenant in the tenants array.
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
'tenants.tenant': {
|
||||||
|
in: getUserTenantIDs(user, 'tenant-admin'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
130
src/collections/Users/endpoints/externalUsersLogin.ts
Normal file
130
src/collections/Users/endpoints/externalUsersLogin.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import type { Collection, Endpoint } from 'payload'
|
||||||
|
|
||||||
|
import { headersWithCors } from '@payloadcms/next/utilities'
|
||||||
|
import { APIError, generatePayloadCookie } from 'payload'
|
||||||
|
|
||||||
|
// A custom endpoint that can be reached by POST request
|
||||||
|
// at: /api/users/external-users/login
|
||||||
|
export const externalUsersLogin: Endpoint = {
|
||||||
|
handler: async (req) => {
|
||||||
|
let data: { [key: string]: string } = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof req.json === 'function') {
|
||||||
|
data = await req.json()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// swallow error, data is already empty object
|
||||||
|
}
|
||||||
|
const { password, tenantSlug, tenantDomain, username } = data
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
throw new APIError('Username and Password are required for login.', 400, null, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullTenant = (
|
||||||
|
await req.payload.find({
|
||||||
|
collection: 'tenants',
|
||||||
|
where: tenantDomain
|
||||||
|
? {
|
||||||
|
domain: {
|
||||||
|
equals: tenantDomain,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
slug: {
|
||||||
|
equals: tenantSlug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).docs[0]
|
||||||
|
|
||||||
|
const foundUser = await req.payload.find({
|
||||||
|
collection: 'users',
|
||||||
|
where: {
|
||||||
|
or: [
|
||||||
|
{
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
email: {
|
||||||
|
equals: username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'tenants.tenant': {
|
||||||
|
equals: fullTenant?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
username: {
|
||||||
|
equals: username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'tenants.tenant': {
|
||||||
|
equals: fullTenant?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (foundUser?.totalDocs > 0) {
|
||||||
|
try {
|
||||||
|
const loginAttempt = await req.payload.login({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: foundUser?.docs[0]?.email || '',
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
req,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loginAttempt?.token) {
|
||||||
|
const collection: Collection = (req.payload.collections as { [key: string]: Collection })[
|
||||||
|
'users'
|
||||||
|
]!
|
||||||
|
const cookie = generatePayloadCookie({
|
||||||
|
collectionAuthConfig: collection.config.auth,
|
||||||
|
cookiePrefix: req.payload.config.cookiePrefix,
|
||||||
|
token: loginAttempt.token,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response.json(loginAttempt, {
|
||||||
|
headers: headersWithCors({
|
||||||
|
headers: new Headers({
|
||||||
|
'Set-Cookie': cookie,
|
||||||
|
}),
|
||||||
|
req,
|
||||||
|
}),
|
||||||
|
status: 200,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new APIError(
|
||||||
|
'Unable to login with the provided username and password.',
|
||||||
|
400,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
throw new APIError(
|
||||||
|
'Unable to login with the provided username and password.',
|
||||||
|
400,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new APIError('Unable to login with the provided username and password.', 400, null, true)
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
path: '/external-users/login',
|
||||||
|
}
|
||||||
76
src/collections/Users/hooks/ensureUniqueUsername.ts
Normal file
76
src/collections/Users/hooks/ensureUniqueUsername.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import type { FieldHook, Where } from 'payload'
|
||||||
|
|
||||||
|
import { ValidationError } from 'payload'
|
||||||
|
|
||||||
|
import { extractID } from '@/utilities/extractID'
|
||||||
|
import { getTenantFromCookie } from '@payloadcms/plugin-multi-tenant/utilities'
|
||||||
|
import { getCollectionIDType } from '@/utilities/getCollectionIDType'
|
||||||
|
import { getUserTenantIDs } from '@/utilities/getUserTenantIds'
|
||||||
|
|
||||||
|
export const ensureUniqueUsername: FieldHook = async ({ data, originalDoc, req, value }) => {
|
||||||
|
// if value is unchanged, skip validation
|
||||||
|
if (originalDoc.username === value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const constraints: Where[] = [
|
||||||
|
{
|
||||||
|
username: {
|
||||||
|
equals: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const selectedTenant = getTenantFromCookie(
|
||||||
|
req.headers,
|
||||||
|
getCollectionIDType({ payload: req.payload, collectionSlug: 'tenants' }),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (selectedTenant) {
|
||||||
|
constraints.push({
|
||||||
|
'tenants.tenant': {
|
||||||
|
equals: selectedTenant,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const findDuplicateUsers = await req.payload.find({
|
||||||
|
collection: 'users',
|
||||||
|
where: {
|
||||||
|
and: constraints,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (findDuplicateUsers.docs.length > 0 && req.user) {
|
||||||
|
const tenantIDs = getUserTenantIDs(req.user)
|
||||||
|
// if the user is an admin or has access to more than 1 tenant
|
||||||
|
// provide a more specific error message
|
||||||
|
if (req.user.roles?.includes('super-admin') || tenantIDs.length > 1) {
|
||||||
|
const attemptedTenantChange = await req.payload.findByID({
|
||||||
|
// @ts-ignore - selectedTenant will match DB ID type
|
||||||
|
id: selectedTenant,
|
||||||
|
collection: 'tenants',
|
||||||
|
})
|
||||||
|
|
||||||
|
throw new ValidationError({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: `The "${attemptedTenantChange.name}" tenant already has a user with the username "${value}". Usernames must be unique per tenant.`,
|
||||||
|
path: 'username',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ValidationError({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: `A user with the username ${value} already exists. Usernames must be unique per tenant.`,
|
||||||
|
path: 'username',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
39
src/collections/Users/hooks/setCookieBasedOnDomain.ts
Normal file
39
src/collections/Users/hooks/setCookieBasedOnDomain.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import type { CollectionAfterLoginHook } from 'payload'
|
||||||
|
|
||||||
|
import { mergeHeaders, generateCookie, getCookieExpiration } from 'payload'
|
||||||
|
|
||||||
|
export const setCookieBasedOnDomain: CollectionAfterLoginHook = async ({ req, user }) => {
|
||||||
|
const relatedOrg = await req.payload.find({
|
||||||
|
collection: 'tenants',
|
||||||
|
depth: 0,
|
||||||
|
limit: 1,
|
||||||
|
where: {
|
||||||
|
domain: {
|
||||||
|
equals: req.headers.get('host'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// If a matching tenant is found, set the 'payload-tenant' cookie
|
||||||
|
if (relatedOrg && relatedOrg.docs.length > 0) {
|
||||||
|
const tenantCookie = generateCookie({
|
||||||
|
name: 'payload-tenant',
|
||||||
|
expires: getCookieExpiration({ seconds: 7200 }),
|
||||||
|
path: '/',
|
||||||
|
returnCookieAsObject: false,
|
||||||
|
value: String(relatedOrg.docs[0].id),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Merge existing responseHeaders with the new Set-Cookie header
|
||||||
|
const newHeaders = new Headers({
|
||||||
|
'Set-Cookie': tenantCookie as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure you merge existing response headers if they already exist
|
||||||
|
req.responseHeaders = req.responseHeaders
|
||||||
|
? mergeHeaders(req.responseHeaders, newHeaders)
|
||||||
|
: newHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
@ -1,7 +1,12 @@
|
|||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
import { authenticated } from '../../access/authenticated'
|
import { createAccess } from './access/create'
|
||||||
import { isSuperAdmin } from '@/access/admin'
|
import { readAccess } from './access/read'
|
||||||
|
import { updateAndDeleteAccess } from './access/updateAndDelete'
|
||||||
|
import { externalUsersLogin } from './endpoints/externalUsersLogin'
|
||||||
|
import { ensureUniqueUsername } from './hooks/ensureUniqueUsername'
|
||||||
|
import { isSuperAdmin } from '@/access/isSuperAdmin'
|
||||||
|
import { setCookieBasedOnDomain } from './hooks/setCookieBasedOnDomain'
|
||||||
import { tenantsArrayField } from '@payloadcms/plugin-multi-tenant/fields'
|
import { tenantsArrayField } from '@payloadcms/plugin-multi-tenant/fields'
|
||||||
|
|
||||||
export enum UserAccessLevel {
|
export enum UserAccessLevel {
|
||||||
@ -35,17 +40,17 @@ const defaultTenantArrayField = tenantsArrayField({
|
|||||||
export const Users: CollectionConfig = {
|
export const Users: CollectionConfig = {
|
||||||
slug: 'users',
|
slug: 'users',
|
||||||
access: {
|
access: {
|
||||||
admin: authenticated,
|
create: createAccess,
|
||||||
create: authenticated,
|
delete: updateAndDeleteAccess,
|
||||||
delete: authenticated,
|
read: readAccess,
|
||||||
read: authenticated,
|
update: updateAndDeleteAccess,
|
||||||
update: authenticated,
|
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
defaultColumns: ['name', 'email'],
|
defaultColumns: ['name', 'email'],
|
||||||
useAsTitle: 'username',
|
useAsTitle: 'username',
|
||||||
},
|
},
|
||||||
auth: true,
|
auth: true,
|
||||||
|
endpoints: [externalUsersLogin],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
admin: {
|
admin: {
|
||||||
@ -56,18 +61,18 @@ export const Users: CollectionConfig = {
|
|||||||
defaultValue: ['guest'],
|
defaultValue: ['guest'],
|
||||||
hasMany: true,
|
hasMany: true,
|
||||||
options: ['full-access', 'super-admin', 'user', 'guest'],
|
options: ['full-access', 'super-admin', 'user', 'guest'],
|
||||||
// access: {
|
access: {
|
||||||
// update: ({ req }) => {
|
update: ({ req }) => {
|
||||||
// return isSuperAdmin({req})
|
return isSuperAdmin(req.user)
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'username',
|
name: 'username',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
// hooks: {
|
hooks: {
|
||||||
// beforeValidate: [ensureUniqueUsername],
|
beforeValidate: [ensureUniqueUsername],
|
||||||
// },
|
},
|
||||||
index: true,
|
index: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -85,4 +90,7 @@ export const Users: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
hooks: {
|
||||||
|
afterLogin: [setCookieBasedOnDomain],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/utilities/extractID.ts
Normal file
10
src/utilities/extractID.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Config } from '@/payload-types'
|
||||||
|
import type { CollectionSlug } from 'payload'
|
||||||
|
|
||||||
|
export const extractID = <T extends Config['collections'][CollectionSlug]>(
|
||||||
|
objectOrID: T | T['id'],
|
||||||
|
): T['id'] => {
|
||||||
|
if (objectOrID && typeof objectOrID === 'object') return objectOrID.id
|
||||||
|
|
||||||
|
return objectOrID
|
||||||
|
}
|
||||||
9
src/utilities/getCollectionIDType.ts
Normal file
9
src/utilities/getCollectionIDType.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { CollectionSlug, Payload } from 'payload'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
collectionSlug: CollectionSlug
|
||||||
|
payload: Payload
|
||||||
|
}
|
||||||
|
export const getCollectionIDType = ({ collectionSlug, payload }: Args): 'number' | 'text' => {
|
||||||
|
return payload.collections[collectionSlug]?.customIDType ?? payload.db.defaultIDType
|
||||||
|
}
|
||||||
31
src/utilities/getUserTenantIds.ts
Normal file
31
src/utilities/getUserTenantIds.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type { Tenant, User } from '@/payload-types'
|
||||||
|
import { extractID } from './extractID'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns array of all tenant IDs assigned to a user
|
||||||
|
*
|
||||||
|
* @param user - User object with tenants field
|
||||||
|
* @param role - Optional role to filter by
|
||||||
|
*/
|
||||||
|
export const getUserTenantIDs = (
|
||||||
|
user: null | User,
|
||||||
|
role?: NonNullable<User['tenants']>[number]['roles'][number],
|
||||||
|
): Tenant['id'][] => {
|
||||||
|
if (!user) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
user?.tenants?.reduce<Tenant['id'][]>((acc, { roles, tenant }) => {
|
||||||
|
if (role && !roles.includes(role)) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tenant) {
|
||||||
|
acc.push(extractID(tenant))
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, []) || []
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user