Compare commits
	
		
			No commits in common. "muilti-tenent" and "main" have entirely different histories.
		
	
	
		
			muilti-ten
			...
			main
		
	
		
							
								
								
									
										141
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										141
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -16,9 +16,7 @@
 | 
			
		||||
        "@payloadcms/db-postgres": "3.31.0",
 | 
			
		||||
        "@payloadcms/next": "3.31.0",
 | 
			
		||||
        "@payloadcms/payload-cloud": "3.31.0",
 | 
			
		||||
        "@payloadcms/plugin-multi-tenant": "^3.39.1",
 | 
			
		||||
        "@payloadcms/richtext-lexical": "3.31.0",
 | 
			
		||||
        "@payloadcms/ui": "^3.39.1",
 | 
			
		||||
        "@radix-ui/react-dialog": "^1.1.11",
 | 
			
		||||
        "@radix-ui/react-dropdown-menu": "^2.1.12",
 | 
			
		||||
        "@radix-ui/react-label": "^2.1.4",
 | 
			
		||||
@ -3954,55 +3952,6 @@
 | 
			
		||||
        "payload": "3.31.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@payloadcms/next/node_modules/@payloadcms/ui": {
 | 
			
		||||
      "version": "3.31.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@payloadcms/ui/-/ui-3.31.0.tgz",
 | 
			
		||||
      "integrity": "sha512-SvRFqCmCo0PCOrwqFeNmL5EoJjGx7712l7pcvyMxpF0RmziZVAzqttnBizO3ha+7z65dJZFmyVHsawhO+iZk1Q==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@date-fns/tz": "1.2.0",
 | 
			
		||||
        "@dnd-kit/core": "6.0.8",
 | 
			
		||||
        "@dnd-kit/sortable": "7.0.2",
 | 
			
		||||
        "@faceless-ui/modal": "3.0.0-beta.2",
 | 
			
		||||
        "@faceless-ui/scroll-info": "2.0.0",
 | 
			
		||||
        "@faceless-ui/window-info": "3.0.1",
 | 
			
		||||
        "@monaco-editor/react": "4.7.0",
 | 
			
		||||
        "@payloadcms/translations": "3.31.0",
 | 
			
		||||
        "bson-objectid": "2.0.4",
 | 
			
		||||
        "date-fns": "4.1.0",
 | 
			
		||||
        "dequal": "2.0.3",
 | 
			
		||||
        "md5": "2.3.0",
 | 
			
		||||
        "object-to-formdata": "4.5.1",
 | 
			
		||||
        "qs-esm": "7.0.2",
 | 
			
		||||
        "react-datepicker": "7.6.0",
 | 
			
		||||
        "react-image-crop": "10.1.8",
 | 
			
		||||
        "react-select": "5.9.0",
 | 
			
		||||
        "scheduler": "0.25.0",
 | 
			
		||||
        "sonner": "^1.7.2",
 | 
			
		||||
        "ts-essentials": "10.0.3",
 | 
			
		||||
        "use-context-selector": "2.0.0",
 | 
			
		||||
        "uuid": "10.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "^18.20.2 || >=20.9.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "next": "^15.2.3",
 | 
			
		||||
        "payload": "3.31.0",
 | 
			
		||||
        "react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020",
 | 
			
		||||
        "react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@payloadcms/next/node_modules/sonner": {
 | 
			
		||||
      "version": "1.7.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
 | 
			
		||||
      "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
 | 
			
		||||
        "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@payloadcms/payload-cloud": {
 | 
			
		||||
      "version": "3.31.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@payloadcms/payload-cloud/-/payload-cloud-3.31.0.tgz",
 | 
			
		||||
@ -4021,17 +3970,6 @@
 | 
			
		||||
        "payload": "3.31.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@payloadcms/plugin-multi-tenant": {
 | 
			
		||||
      "version": "3.39.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@payloadcms/plugin-multi-tenant/-/plugin-multi-tenant-3.39.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Uag8YRWoBwiUGNMdHCY1fJ7QNDPcw1nVGaHeSim1j+18NgiRTKsQD18xULei3UsrdYb9/mll7wZ5b3nPYauQGQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@payloadcms/ui": "3.39.1",
 | 
			
		||||
        "next": "^15.2.3",
 | 
			
		||||
        "payload": "3.39.1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@payloadcms/richtext-lexical": {
 | 
			
		||||
      "version": "3.31.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@payloadcms/richtext-lexical/-/richtext-lexical-3.31.0.tgz",
 | 
			
		||||
@ -4077,7 +4015,16 @@
 | 
			
		||||
        "react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@payloadcms/richtext-lexical/node_modules/@payloadcms/ui": {
 | 
			
		||||
    "node_modules/@payloadcms/translations": {
 | 
			
		||||
      "version": "3.31.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@payloadcms/translations/-/translations-3.31.0.tgz",
 | 
			
		||||
      "integrity": "sha512-vjbBuHJUZ04R7wkOR1+QhZRO1xG7bvkLgx6zoiKZZmvItqiPA5ZWsyrq3NFhviOH26dH2tOdnO+RLPuaElkWFg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "date-fns": "4.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@payloadcms/ui": {
 | 
			
		||||
      "version": "3.31.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@payloadcms/ui/-/ui-3.31.0.tgz",
 | 
			
		||||
      "integrity": "sha512-SvRFqCmCo0PCOrwqFeNmL5EoJjGx7712l7pcvyMxpF0RmziZVAzqttnBizO3ha+7z65dJZFmyVHsawhO+iZk1Q==",
 | 
			
		||||
@ -4116,74 +4063,6 @@
 | 
			
		||||
        "react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@payloadcms/richtext-lexical/node_modules/sonner": {
 | 
			
		||||
      "version": "1.7.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
 | 
			
		||||
      "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
 | 
			
		||||
        "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@payloadcms/translations": {
 | 
			
		||||
      "version": "3.31.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@payloadcms/translations/-/translations-3.31.0.tgz",
 | 
			
		||||
      "integrity": "sha512-vjbBuHJUZ04R7wkOR1+QhZRO1xG7bvkLgx6zoiKZZmvItqiPA5ZWsyrq3NFhviOH26dH2tOdnO+RLPuaElkWFg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "date-fns": "4.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@payloadcms/ui": {
 | 
			
		||||
      "version": "3.39.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@payloadcms/ui/-/ui-3.39.1.tgz",
 | 
			
		||||
      "integrity": "sha512-kjnLYSFgmqdyTPF4ecqqRUyHFb4hQalZ6Z/6qs6Sz9XYZHgY5hwN6MJIL6L/MDh0Tv7acaRUlTCQNWjzjEU79g==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@date-fns/tz": "1.2.0",
 | 
			
		||||
        "@dnd-kit/core": "6.0.8",
 | 
			
		||||
        "@dnd-kit/sortable": "7.0.2",
 | 
			
		||||
        "@dnd-kit/utilities": "3.2.2",
 | 
			
		||||
        "@faceless-ui/modal": "3.0.0-beta.2",
 | 
			
		||||
        "@faceless-ui/scroll-info": "2.0.0",
 | 
			
		||||
        "@faceless-ui/window-info": "3.0.1",
 | 
			
		||||
        "@monaco-editor/react": "4.7.0",
 | 
			
		||||
        "@payloadcms/translations": "3.39.1",
 | 
			
		||||
        "bson-objectid": "2.0.4",
 | 
			
		||||
        "date-fns": "4.1.0",
 | 
			
		||||
        "dequal": "2.0.3",
 | 
			
		||||
        "md5": "2.3.0",
 | 
			
		||||
        "object-to-formdata": "4.5.1",
 | 
			
		||||
        "qs-esm": "7.0.2",
 | 
			
		||||
        "react-datepicker": "7.6.0",
 | 
			
		||||
        "react-image-crop": "10.1.8",
 | 
			
		||||
        "react-select": "5.9.0",
 | 
			
		||||
        "scheduler": "0.25.0",
 | 
			
		||||
        "sonner": "^1.7.2",
 | 
			
		||||
        "ts-essentials": "10.0.3",
 | 
			
		||||
        "use-context-selector": "2.0.0",
 | 
			
		||||
        "uuid": "10.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": "^18.20.2 || >=20.9.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "next": "^15.2.3",
 | 
			
		||||
        "payload": "3.39.1",
 | 
			
		||||
        "react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020",
 | 
			
		||||
        "react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@payloadcms/ui/node_modules/@payloadcms/translations": {
 | 
			
		||||
      "version": "3.39.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@payloadcms/translations/-/translations-3.39.1.tgz",
 | 
			
		||||
      "integrity": "sha512-GwU6lwpi5hEijaE64dRNPIF1x8J2aN4loR+Z7Hqk2DP5UqtHTRmNx6LLTzanUfwvOiyR1nnFpCsHyAC57c8gtw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "date-fns": "4.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@payloadcms/ui/node_modules/sonner": {
 | 
			
		||||
      "version": "1.7.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
 | 
			
		||||
 | 
			
		||||
@ -23,9 +23,7 @@
 | 
			
		||||
    "@payloadcms/db-postgres": "3.31.0",
 | 
			
		||||
    "@payloadcms/next": "3.31.0",
 | 
			
		||||
    "@payloadcms/payload-cloud": "3.31.0",
 | 
			
		||||
    "@payloadcms/plugin-multi-tenant": "^3.39.1",
 | 
			
		||||
    "@payloadcms/richtext-lexical": "3.31.0",
 | 
			
		||||
    "@payloadcms/ui": "^3.39.1",
 | 
			
		||||
    "@radix-ui/react-dialog": "^1.1.11",
 | 
			
		||||
    "@radix-ui/react-dropdown-menu": "^2.1.12",
 | 
			
		||||
    "@radix-ui/react-label": "^2.1.4",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								src/access/admin.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/access/admin.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
import { User } from '@/payload-types'
 | 
			
		||||
import type { AccessArgs } from 'payload'
 | 
			
		||||
 | 
			
		||||
type isAdmin = (args: AccessArgs<User>) => boolean
 | 
			
		||||
 | 
			
		||||
export const admin: isAdmin = ({ req: { user } }) => user?.role === 'admin'
 | 
			
		||||
 | 
			
		||||
@ -1,13 +0,0 @@
 | 
			
		||||
import type { Access } from 'payload'
 | 
			
		||||
 | 
			
		||||
export const authenticatedOrPublished: Access = ({ req: { user } }) => {
 | 
			
		||||
  if (user) {
 | 
			
		||||
    return true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    _status: {
 | 
			
		||||
      equals: 'published',
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,10 +0,0 @@
 | 
			
		||||
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'))
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
import { WatchTenantCollection as WatchTenantCollection_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
 | 
			
		||||
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 { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
 | 
			
		||||
@ -22,12 +20,8 @@ import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93
 | 
			
		||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
 | 
			
		||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
 | 
			
		||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
 | 
			
		||||
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/plugin-multi-tenant/client#WatchTenantCollection": WatchTenantCollection_1d0591e3cf4f332c83a86da13a0de59a,
 | 
			
		||||
  "@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/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
 | 
			
		||||
@ -49,7 +43,5 @@ export const importMap = {
 | 
			
		||||
  "@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
 | 
			
		||||
  "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
 | 
			
		||||
  "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
 | 
			
		||||
  "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
 | 
			
		||||
  "@payloadcms/plugin-multi-tenant/client#TenantSelector": TenantSelector_1d0591e3cf4f332c83a86da13a0de59a,
 | 
			
		||||
  "@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62
 | 
			
		||||
  "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
//import { admin } from '@/access/admin'
 | 
			
		||||
import { admin } from '@/access/admin'
 | 
			
		||||
import { authenticated } from '@/access/authenticated'
 | 
			
		||||
import type { CollectionConfig } from 'payload'
 | 
			
		||||
 | 
			
		||||
@ -12,9 +12,9 @@ export const Authors: CollectionConfig = {
 | 
			
		||||
  },
 | 
			
		||||
  access: {
 | 
			
		||||
    read: () => true,
 | 
			
		||||
    //    update: admin,
 | 
			
		||||
    //    create: authenticated,
 | 
			
		||||
    //    delete: admin,
 | 
			
		||||
    update: admin,
 | 
			
		||||
    create: authenticated,
 | 
			
		||||
    delete: admin,
 | 
			
		||||
  },
 | 
			
		||||
  fields: [
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { admin } from "@/access/admin";
 | 
			
		||||
import { authenticated } from "@/access/authenticated";
 | 
			
		||||
import { CollectionConfig } from "payload";
 | 
			
		||||
 | 
			
		||||
@ -8,9 +9,9 @@ export const Books: CollectionConfig = {
 | 
			
		||||
  },
 | 
			
		||||
  access: {
 | 
			
		||||
    read: () => true,
 | 
			
		||||
    //    update: admin,
 | 
			
		||||
    //    create: authenticated,
 | 
			
		||||
    //    delete: admin,
 | 
			
		||||
    update: admin,
 | 
			
		||||
    create: authenticated,
 | 
			
		||||
    delete: admin,
 | 
			
		||||
  },
 | 
			
		||||
  fields: [
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { admin } from "@/access/admin";
 | 
			
		||||
import { CollectionConfig } from "payload";
 | 
			
		||||
 | 
			
		||||
export const Genre: CollectionConfig = {
 | 
			
		||||
@ -7,9 +8,9 @@ export const Genre: CollectionConfig = {
 | 
			
		||||
  },
 | 
			
		||||
  access: {
 | 
			
		||||
    read: () => true,
 | 
			
		||||
    //    update: admin,
 | 
			
		||||
    //    create: () => true,
 | 
			
		||||
    //    delete: admin,
 | 
			
		||||
    update: admin,
 | 
			
		||||
    create: () => true,
 | 
			
		||||
    delete: admin,
 | 
			
		||||
  },
 | 
			
		||||
  fields: [
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { admin } from "@/access/admin";
 | 
			
		||||
import { authenticated } from "@/access/authenticated";
 | 
			
		||||
import { CollectionConfig } from "payload";
 | 
			
		||||
 | 
			
		||||
@ -5,9 +6,9 @@ const Checkouts: CollectionConfig = {
 | 
			
		||||
  slug: 'checkouts',
 | 
			
		||||
  access: {
 | 
			
		||||
    read: () => true,
 | 
			
		||||
    //   update: admin,
 | 
			
		||||
    //   create: authenticated,
 | 
			
		||||
    //   delete: admin,
 | 
			
		||||
    update: admin,
 | 
			
		||||
    create: authenticated,
 | 
			
		||||
    delete: admin,
 | 
			
		||||
  },
 | 
			
		||||
  fields: [
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { admin } from "@/access/admin";
 | 
			
		||||
import { authenticated } from "@/access/authenticated";
 | 
			
		||||
import { CollectionConfig } from "payload";
 | 
			
		||||
 | 
			
		||||
@ -5,9 +6,9 @@ const HoldRequests: CollectionConfig = {
 | 
			
		||||
  slug: 'holdRequests',
 | 
			
		||||
  access: {
 | 
			
		||||
    read: () => true,
 | 
			
		||||
    //   update: admin,
 | 
			
		||||
    //    create: authenticated,
 | 
			
		||||
    //    delete: admin,
 | 
			
		||||
    update: admin,
 | 
			
		||||
    create: authenticated,
 | 
			
		||||
    delete: admin,
 | 
			
		||||
  },
 | 
			
		||||
  fields: [
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { admin } from "@/access/admin";
 | 
			
		||||
import { authenticated } from "@/access/authenticated";
 | 
			
		||||
import { CollectionConfig } from "payload";
 | 
			
		||||
 | 
			
		||||
@ -8,9 +9,9 @@ export const Repositories: CollectionConfig = {
 | 
			
		||||
  },
 | 
			
		||||
  access: {
 | 
			
		||||
    read: () => true,
 | 
			
		||||
    //    update: admin,
 | 
			
		||||
    //    create: authenticated,
 | 
			
		||||
    //    delete: admin,
 | 
			
		||||
    update: admin,
 | 
			
		||||
    create: authenticated,
 | 
			
		||||
    delete: admin,
 | 
			
		||||
  },
 | 
			
		||||
  fields: [
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -1,39 +0,0 @@
 | 
			
		||||
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) || [],
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
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,51 +0,0 @@
 | 
			
		||||
import type { CollectionConfig } from 'payload'
 | 
			
		||||
 | 
			
		||||
import { isSuperAdminAccess } from '@/access/isSuperAdmin'
 | 
			
		||||
import { updateAndDeleteAccess } from './access/updateAndDelete'
 | 
			
		||||
 | 
			
		||||
export const Tenants: CollectionConfig = {
 | 
			
		||||
  slug: 'tenants',
 | 
			
		||||
  access: {
 | 
			
		||||
    create: isSuperAdminAccess,
 | 
			
		||||
    delete: updateAndDeleteAccess,
 | 
			
		||||
    read: ({ req }) => Boolean(req.user),
 | 
			
		||||
    update: updateAndDeleteAccess,
 | 
			
		||||
  },
 | 
			
		||||
  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,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
}
 | 
			
		||||
@ -1,24 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
import { User } from '@/payload-types'
 | 
			
		||||
 | 
			
		||||
export const isAccessingSelf = ({ id, user }: { user?: User; id?: string | number }): boolean => {
 | 
			
		||||
  return user ? Boolean(user.id === id) : false
 | 
			
		||||
}
 | 
			
		||||
@ -1,54 +0,0 @@
 | 
			
		||||
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)
 | 
			
		||||
  if (superAdmin) return true;
 | 
			
		||||
 | 
			
		||||
  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,
 | 
			
		||||
        },
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    or: [
 | 
			
		||||
      {
 | 
			
		||||
        id: {
 | 
			
		||||
          equals: req.user.id,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        'tenants.tenant': {
 | 
			
		||||
          in: adminTenantAccessIDs,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  } as Where
 | 
			
		||||
}
 | 
			
		||||
@ -1,31 +0,0 @@
 | 
			
		||||
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'),
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,130 +0,0 @@
 | 
			
		||||
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',
 | 
			
		||||
}
 | 
			
		||||
@ -1,76 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,39 +0,0 @@
 | 
			
		||||
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,111 +0,0 @@
 | 
			
		||||
import type { CollectionConfig } from 'payload'
 | 
			
		||||
 | 
			
		||||
import { createAccess } from './access/create'
 | 
			
		||||
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'
 | 
			
		||||
 | 
			
		||||
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-client'],
 | 
			
		||||
      hasMany: true,
 | 
			
		||||
      options: ['tenant-admin', 'tenant-client', 'tenant-owner'],
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export const Users: CollectionConfig = {
 | 
			
		||||
  slug: 'users',
 | 
			
		||||
  access: {
 | 
			
		||||
    create: createAccess,
 | 
			
		||||
    delete: updateAndDeleteAccess,
 | 
			
		||||
    read: readAccess,
 | 
			
		||||
    update: updateAndDeleteAccess,
 | 
			
		||||
  },
 | 
			
		||||
  admin: {
 | 
			
		||||
    defaultColumns: ['name', 'email'],
 | 
			
		||||
    useAsTitle: 'username',
 | 
			
		||||
    group: 'User Data',
 | 
			
		||||
  },
 | 
			
		||||
  auth: true,
 | 
			
		||||
  endpoints: [externalUsersLogin],
 | 
			
		||||
  fields: [
 | 
			
		||||
    {
 | 
			
		||||
      admin: {
 | 
			
		||||
        position: 'sidebar',
 | 
			
		||||
      },
 | 
			
		||||
      name: 'roles',
 | 
			
		||||
      type: 'select',
 | 
			
		||||
      defaultValue: ['guest'],
 | 
			
		||||
      hasMany: true,
 | 
			
		||||
      options: ['full-access', 'super-admin', 'user', 'guest'],
 | 
			
		||||
      access: {
 | 
			
		||||
        update: ({ req }) => {
 | 
			
		||||
          return isSuperAdmin(req.user)
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'username',
 | 
			
		||||
      type: 'text',
 | 
			
		||||
      hooks: {
 | 
			
		||||
        beforeValidate: [ensureUniqueUsername],
 | 
			
		||||
      },
 | 
			
		||||
      index: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'firstName',
 | 
			
		||||
      type: 'text',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'lastName',
 | 
			
		||||
      type: 'text',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'repositories',
 | 
			
		||||
      type: 'join',
 | 
			
		||||
      collection: 'repositories',
 | 
			
		||||
      on: 'owner',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'profilePicture',
 | 
			
		||||
      type: 'relationship',
 | 
			
		||||
      relationTo: 'media',
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      ...defaultTenantArrayField,
 | 
			
		||||
      admin: {
 | 
			
		||||
        ...(defaultTenantArrayField?.admin || {}),
 | 
			
		||||
        position: 'sidebar',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  timestamps: true,
 | 
			
		||||
  hooks: {
 | 
			
		||||
    afterLogin: [setCookieBasedOnDomain],
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
import type { FieldHook } from 'payload'
 | 
			
		||||
 | 
			
		||||
const format = (val: string): string =>
 | 
			
		||||
  val
 | 
			
		||||
    .replace(/ /g, '-')
 | 
			
		||||
    .replace(/[^\w-]+/g, '')
 | 
			
		||||
    .toLowerCase()
 | 
			
		||||
 | 
			
		||||
const formatSlug =
 | 
			
		||||
  (fallback: string): FieldHook =>
 | 
			
		||||
  ({ data, operation, originalDoc, value }) => {
 | 
			
		||||
    if (typeof value === 'string') {
 | 
			
		||||
      return format(value)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (operation === 'create') {
 | 
			
		||||
      const fallbackData = data?.[fallback] || originalDoc?.[fallback]
 | 
			
		||||
 | 
			
		||||
      if (fallbackData && typeof fallbackData === 'string') {
 | 
			
		||||
        return format(fallbackData)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return value
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
export default formatSlug
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
import type { CollectionBeforeChangeHook } from 'payload'
 | 
			
		||||
 | 
			
		||||
export const populatePublishedAt: CollectionBeforeChangeHook = ({ data, operation, req }) => {
 | 
			
		||||
  if (operation === 'create' || operation === 'update') {
 | 
			
		||||
    if (req.data && !req.data.publishedAt) {
 | 
			
		||||
      const now = new Date()
 | 
			
		||||
      return {
 | 
			
		||||
        ...data,
 | 
			
		||||
        publishedAt: now,
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return data
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
import type { CollectionAfterChangeHook } from 'payload'
 | 
			
		||||
 | 
			
		||||
import { revalidateTag } from 'next/cache'
 | 
			
		||||
 | 
			
		||||
export const revalidateRedirects: CollectionAfterChangeHook = ({ doc, req: { payload } }) => {
 | 
			
		||||
  payload.logger.info(`Revalidating redirects`)
 | 
			
		||||
 | 
			
		||||
  revalidateTag('redirects')
 | 
			
		||||
 | 
			
		||||
  return doc
 | 
			
		||||
}
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
const MOBILE_BREAKPOINT = 768
 | 
			
		||||
 | 
			
		||||
export function useIsMobile() {
 | 
			
		||||
  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
 | 
			
		||||
    const onChange = () => {
 | 
			
		||||
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
 | 
			
		||||
    }
 | 
			
		||||
    mql.addEventListener("change", onChange)
 | 
			
		||||
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
 | 
			
		||||
    return () => mql.removeEventListener("change", onChange)
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return !!isMobile
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
// storage-adapter-import-placeholder
 | 
			
		||||
import { postgresAdapter } from '@payloadcms/db-postgres'
 | 
			
		||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
 | 
			
		||||
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
 | 
			
		||||
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
 | 
			
		||||
import path from 'path'
 | 
			
		||||
import { buildConfig } from 'payload'
 | 
			
		||||
@ -19,12 +18,8 @@ import { Header } from './globals/header/config'
 | 
			
		||||
import { Pages } from './collections/Pages/Pages'
 | 
			
		||||
import HoldRequests from './collections/Checkouts/HoldRequests'
 | 
			
		||||
import Checkouts from './collections/Checkouts/Checkouts'
 | 
			
		||||
import { isSuperAdmin } from '@/access/isSuperAdmin'
 | 
			
		||||
 | 
			
		||||
import './envConfig.ts'
 | 
			
		||||
import { Config, User } from './payload-types'
 | 
			
		||||
import { getUserTenantIDs } from './utilities/getUserTenantIds'
 | 
			
		||||
import { Tenants } from './collections/Tenants'
 | 
			
		||||
 | 
			
		||||
const filename = fileURLToPath(import.meta.url)
 | 
			
		||||
const dirname = path.dirname(filename)
 | 
			
		||||
@ -45,7 +40,7 @@ export default buildConfig({
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  globals: [Header],
 | 
			
		||||
  collections: [Tenants, Users, Media, Books, Authors, Repositories, Copies, HoldRequests, Checkouts, Genre, Pages],
 | 
			
		||||
  collections: [Users, Media, Books, Authors, Repositories, Copies, HoldRequests, Checkouts, Genre, Pages],
 | 
			
		||||
  editor: lexicalEditor(),
 | 
			
		||||
  secret: process.env.PAYLOAD_SECRET || '',
 | 
			
		||||
  typescript: {
 | 
			
		||||
@ -72,33 +67,6 @@ export default buildConfig({
 | 
			
		||||
  plugins: [
 | 
			
		||||
    //payloadCloudPlugin(),
 | 
			
		||||
    // storage-adapter-placeholder
 | 
			
		||||
    multiTenantPlugin<Config>({
 | 
			
		||||
      debug: true,
 | 
			
		||||
      enabled: true,
 | 
			
		||||
      collections: {
 | 
			
		||||
        pages: {},
 | 
			
		||||
        books: {},
 | 
			
		||||
        copies: {},
 | 
			
		||||
        repositories: {},
 | 
			
		||||
        checkouts: {},
 | 
			
		||||
        holdRequests: {},
 | 
			
		||||
      },
 | 
			
		||||
      tenantsArrayField: {
 | 
			
		||||
        includeDefaultField: false,
 | 
			
		||||
      },
 | 
			
		||||
      userHasAccessToAllTenants: (user: User) => isSuperAdmin(user),
 | 
			
		||||
      tenantField: {
 | 
			
		||||
        access: {
 | 
			
		||||
          read: () => true,
 | 
			
		||||
          update: ({ req }) => {
 | 
			
		||||
            if (isSuperAdmin(req.user)) {
 | 
			
		||||
              return true
 | 
			
		||||
            }
 | 
			
		||||
            return getUserTenantIDs(req.user).length > 0
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
  ],
 | 
			
		||||
  hooks: {
 | 
			
		||||
    afterError: [
 | 
			
		||||
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
export default !!(typeof window !== 'undefined' && window.document && window.document.createElement)
 | 
			
		||||
@ -1,35 +0,0 @@
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 | 
			
		||||
// @ts-nocheck
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Simple object check.
 | 
			
		||||
 * @param item
 | 
			
		||||
 * @returns {boolean}
 | 
			
		||||
 */
 | 
			
		||||
export function isObject(item: unknown): item is object {
 | 
			
		||||
  return typeof item === 'object' && !Array.isArray(item)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Deep merge two objects.
 | 
			
		||||
 * @param target
 | 
			
		||||
 * @param ...sources
 | 
			
		||||
 */
 | 
			
		||||
export default function deepMerge<T, R>(target: T, source: R): T {
 | 
			
		||||
  const output = { ...target }
 | 
			
		||||
  if (isObject(target) && isObject(source)) {
 | 
			
		||||
    Object.keys(source).forEach((key) => {
 | 
			
		||||
      if (isObject(source[key])) {
 | 
			
		||||
        if (!(key in target)) {
 | 
			
		||||
          Object.assign(output, { [key]: source[key] })
 | 
			
		||||
        } else {
 | 
			
		||||
          output[key] = deepMerge(target[key], source[key])
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        Object.assign(output, { [key]: source[key] })
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return output
 | 
			
		||||
}
 | 
			
		||||
@ -1,10 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,24 +0,0 @@
 | 
			
		||||
import { Post } from '@/payload-types'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Formats an array of populatedAuthors from Posts into a prettified string.
 | 
			
		||||
 * @param authors - The populatedAuthors array from a Post.
 | 
			
		||||
 * @returns A prettified string of authors.
 | 
			
		||||
 * @example
 | 
			
		||||
 *
 | 
			
		||||
 * [Author1, Author2] becomes 'Author1 and Author2'
 | 
			
		||||
 * [Author1, Author2, Author3] becomes 'Author1, Author2, and Author3'
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export const formatAuthors = (
 | 
			
		||||
  authors: NonNullable<NonNullable<Post['populatedAuthors']>[number]>[],
 | 
			
		||||
) => {
 | 
			
		||||
  // Ensure we don't have any authors without a name
 | 
			
		||||
  const authorNames = authors.map((author) => author.name).filter(Boolean)
 | 
			
		||||
 | 
			
		||||
  if (authorNames.length === 0) return ''
 | 
			
		||||
  if (authorNames.length === 1) return authorNames[0]
 | 
			
		||||
  if (authorNames.length === 2) return `${authorNames[0]} and ${authorNames[1]}`
 | 
			
		||||
 | 
			
		||||
  return `${authorNames.slice(0, -1).join(', ')} and ${authorNames[authorNames.length - 1]}`
 | 
			
		||||
}
 | 
			
		||||
@ -1,20 +0,0 @@
 | 
			
		||||
export const formatDateTime = (timestamp: string): string => {
 | 
			
		||||
  const now = new Date()
 | 
			
		||||
  let date = now
 | 
			
		||||
  if (timestamp) date = new Date(timestamp)
 | 
			
		||||
  const months = date.getMonth()
 | 
			
		||||
  const days = date.getDate()
 | 
			
		||||
  // const hours = date.getHours();
 | 
			
		||||
  // const minutes = date.getMinutes();
 | 
			
		||||
  // const seconds = date.getSeconds();
 | 
			
		||||
 | 
			
		||||
  const MM = months + 1 < 10 ? `0${months + 1}` : months + 1
 | 
			
		||||
  const DD = days < 10 ? `0${days}` : days
 | 
			
		||||
  const YYYY = date.getFullYear()
 | 
			
		||||
  // const AMPM = hours < 12 ? 'AM' : 'PM';
 | 
			
		||||
  // const HH = hours > 12 ? hours - 12 : hours;
 | 
			
		||||
  // const MinMin = (minutes < 10) ? `0${minutes}` : minutes;
 | 
			
		||||
  // const SS = (seconds < 10) ? `0${seconds}` : seconds;
 | 
			
		||||
 | 
			
		||||
  return `${MM}/${DD}/${YYYY}`
 | 
			
		||||
}
 | 
			
		||||
@ -1,49 +0,0 @@
 | 
			
		||||
import type { Metadata } from 'next'
 | 
			
		||||
 | 
			
		||||
import type { Media, Page, Post, Config } from '../payload-types'
 | 
			
		||||
 | 
			
		||||
import { mergeOpenGraph } from './mergeOpenGraph'
 | 
			
		||||
import { getServerSideURL } from './getURL'
 | 
			
		||||
 | 
			
		||||
const getImageURL = (image?: Media | Config['db']['defaultIDType'] | null) => {
 | 
			
		||||
  const serverUrl = getServerSideURL()
 | 
			
		||||
 | 
			
		||||
  let url = serverUrl + '/website-template-OG.webp'
 | 
			
		||||
 | 
			
		||||
  if (image && typeof image === 'object' && 'url' in image) {
 | 
			
		||||
    const ogUrl = image.sizes?.og?.url
 | 
			
		||||
 | 
			
		||||
    url = ogUrl ? serverUrl + ogUrl : serverUrl + image.url
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return url
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const generateMeta = async (args: {
 | 
			
		||||
  doc: Partial<Page> | Partial<Post> | null
 | 
			
		||||
}): Promise<Metadata> => {
 | 
			
		||||
  const { doc } = args
 | 
			
		||||
 | 
			
		||||
  const ogImage = getImageURL(doc?.meta?.image)
 | 
			
		||||
 | 
			
		||||
  const title = doc?.meta?.title
 | 
			
		||||
    ? doc?.meta?.title + ' | Payload Website Template'
 | 
			
		||||
    : 'Payload Website Template'
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    description: doc?.meta?.description,
 | 
			
		||||
    openGraph: mergeOpenGraph({
 | 
			
		||||
      description: doc?.meta?.description || '',
 | 
			
		||||
      images: ogImage
 | 
			
		||||
        ? [
 | 
			
		||||
            {
 | 
			
		||||
              url: ogImage,
 | 
			
		||||
            },
 | 
			
		||||
          ]
 | 
			
		||||
        : undefined,
 | 
			
		||||
      title,
 | 
			
		||||
      url: Array.isArray(doc?.slug) ? doc?.slug.join('/') : '/',
 | 
			
		||||
    }),
 | 
			
		||||
    title,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,25 +0,0 @@
 | 
			
		||||
import { PayloadRequest, CollectionSlug } from 'payload'
 | 
			
		||||
 | 
			
		||||
const collectionPrefixMap: Partial<Record<CollectionSlug, string>> = {
 | 
			
		||||
  posts: '/posts',
 | 
			
		||||
  pages: '',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  collection: keyof typeof collectionPrefixMap
 | 
			
		||||
  slug: string
 | 
			
		||||
  req: PayloadRequest
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const generatePreviewPath = ({ collection, slug }: Props) => {
 | 
			
		||||
  const encodedParams = new URLSearchParams({
 | 
			
		||||
    slug,
 | 
			
		||||
    collection,
 | 
			
		||||
    path: `${collectionPrefixMap[collection]}/${slug}`,
 | 
			
		||||
    previewSecret: process.env.PREVIEW_SECRET || '',
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const url = `/next/preview?${encodedParams.toString()}`
 | 
			
		||||
 | 
			
		||||
  return url
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
@ -1,31 +0,0 @@
 | 
			
		||||
import type { Config } from 'src/payload-types'
 | 
			
		||||
 | 
			
		||||
import configPromise from '@payload-config'
 | 
			
		||||
import { getPayload } from 'payload'
 | 
			
		||||
import { unstable_cache } from 'next/cache'
 | 
			
		||||
 | 
			
		||||
type Collection = keyof Config['collections']
 | 
			
		||||
 | 
			
		||||
async function getDocument(collection: Collection, slug: string, depth = 0) {
 | 
			
		||||
  const payload = await getPayload({ config: configPromise })
 | 
			
		||||
 | 
			
		||||
  const page = await payload.find({
 | 
			
		||||
    collection,
 | 
			
		||||
    depth,
 | 
			
		||||
    where: {
 | 
			
		||||
      slug: {
 | 
			
		||||
        equals: slug,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return page.docs[0]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns a unstable_cache function mapped with the cache tag for the slug
 | 
			
		||||
 */
 | 
			
		||||
export const getCachedDocument = (collection: Collection, slug: string) =>
 | 
			
		||||
  unstable_cache(async () => getDocument(collection, slug), [collection, slug], {
 | 
			
		||||
    tags: [`${collection}_${slug}`],
 | 
			
		||||
  })
 | 
			
		||||
@ -1,26 +0,0 @@
 | 
			
		||||
import type { Config } from 'src/payload-types'
 | 
			
		||||
 | 
			
		||||
import configPromise from '@payload-config'
 | 
			
		||||
import { getPayload } from 'payload'
 | 
			
		||||
import { unstable_cache } from 'next/cache'
 | 
			
		||||
 | 
			
		||||
type Global = keyof Config['globals']
 | 
			
		||||
 | 
			
		||||
async function getGlobal(slug: Global, depth = 0) {
 | 
			
		||||
  const payload = await getPayload({ config: configPromise })
 | 
			
		||||
 | 
			
		||||
  const global = await payload.findGlobal({
 | 
			
		||||
    slug,
 | 
			
		||||
    depth,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return global
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns a unstable_cache function mapped with the cache tag for the slug
 | 
			
		||||
 */
 | 
			
		||||
export const getCachedGlobal = (slug: Global, depth = 0) =>
 | 
			
		||||
  unstable_cache(async () => getGlobal(slug, depth), [slug], {
 | 
			
		||||
    tags: [`global_${slug}`],
 | 
			
		||||
  })
 | 
			
		||||
@ -1,43 +0,0 @@
 | 
			
		||||
import { cookies } from 'next/headers'
 | 
			
		||||
import { redirect } from 'next/navigation'
 | 
			
		||||
 | 
			
		||||
import type { User } from '../payload-types'
 | 
			
		||||
import { getClientSideURL } from './getURL'
 | 
			
		||||
 | 
			
		||||
export const getMeUser = async (args?: {
 | 
			
		||||
  nullUserRedirect?: string
 | 
			
		||||
  validUserRedirect?: string
 | 
			
		||||
}): Promise<{
 | 
			
		||||
  token: string
 | 
			
		||||
  user: User
 | 
			
		||||
}> => {
 | 
			
		||||
  const { nullUserRedirect, validUserRedirect } = args || {}
 | 
			
		||||
  const cookieStore = await cookies()
 | 
			
		||||
  const token = cookieStore.get('payload-token')?.value
 | 
			
		||||
 | 
			
		||||
  const meUserReq = await fetch(`${getClientSideURL()}/api/users/me`, {
 | 
			
		||||
    headers: {
 | 
			
		||||
      Authorization: `JWT ${token}`,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    user,
 | 
			
		||||
  }: {
 | 
			
		||||
    user: User
 | 
			
		||||
  } = await meUserReq.json()
 | 
			
		||||
 | 
			
		||||
  if (validUserRedirect && meUserReq.ok && user) {
 | 
			
		||||
    redirect(validUserRedirect)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (nullUserRedirect && (!meUserReq.ok || !user)) {
 | 
			
		||||
    redirect(nullUserRedirect)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Token will exist here because if it doesn't the user will be redirected
 | 
			
		||||
  return {
 | 
			
		||||
    token: token!,
 | 
			
		||||
    user,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,26 +0,0 @@
 | 
			
		||||
import configPromise from '@payload-config'
 | 
			
		||||
import { getPayload } from 'payload'
 | 
			
		||||
import { unstable_cache } from 'next/cache'
 | 
			
		||||
 | 
			
		||||
export async function getRedirects(depth = 1) {
 | 
			
		||||
  const payload = await getPayload({ config: configPromise })
 | 
			
		||||
 | 
			
		||||
  const { docs: redirects } = await payload.find({
 | 
			
		||||
    collection: 'redirects',
 | 
			
		||||
    depth,
 | 
			
		||||
    limit: 0,
 | 
			
		||||
    pagination: false,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return redirects
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns a unstable_cache function mapped with the cache tag for 'redirects'.
 | 
			
		||||
 *
 | 
			
		||||
 * Cache all redirects together to avoid multiple fetches.
 | 
			
		||||
 */
 | 
			
		||||
export const getCachedRedirects = () =>
 | 
			
		||||
  unstable_cache(async () => getRedirects(), ['redirects'], {
 | 
			
		||||
    tags: ['redirects'],
 | 
			
		||||
  })
 | 
			
		||||
@ -1,31 +0,0 @@
 | 
			
		||||
import canUseDOM from './canUseDOM'
 | 
			
		||||
 | 
			
		||||
export const getServerSideURL = () => {
 | 
			
		||||
  let url = process.env.NEXT_PUBLIC_SERVER_URL
 | 
			
		||||
 | 
			
		||||
  if (!url && process.env.VERCEL_PROJECT_PRODUCTION_URL) {
 | 
			
		||||
    return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!url) {
 | 
			
		||||
    url = 'http://localhost:3000'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return url
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getClientSideURL = () => {
 | 
			
		||||
  if (canUseDOM) {
 | 
			
		||||
    const protocol = window.location.protocol
 | 
			
		||||
    const domain = window.location.hostname
 | 
			
		||||
    const port = window.location.port
 | 
			
		||||
 | 
			
		||||
    return `${protocol}//${domain}${port ? `:${port}` : ''}`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (process.env.VERCEL_PROJECT_PRODUCTION_URL) {
 | 
			
		||||
    return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return process.env.NEXT_PUBLIC_SERVER_URL || ''
 | 
			
		||||
}
 | 
			
		||||
@ -1,31 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
    }, []) || []
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
const makeAcronym = (name: string, maxLength: number = 2) => {
 | 
			
		||||
  return name
 | 
			
		||||
    .split(' ')
 | 
			
		||||
    .map((part) => part[0])
 | 
			
		||||
    .slice(0, maxLength)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default makeAcronym
 | 
			
		||||
@ -1,22 +0,0 @@
 | 
			
		||||
import type { Metadata } from 'next'
 | 
			
		||||
import { getServerSideURL } from './getURL'
 | 
			
		||||
 | 
			
		||||
const defaultOpenGraph: Metadata['openGraph'] = {
 | 
			
		||||
  type: 'website',
 | 
			
		||||
  description: 'An open-source website built with Payload and Next.js.',
 | 
			
		||||
  images: [
 | 
			
		||||
    {
 | 
			
		||||
      url: `${getServerSideURL()}/website-template-OG.webp`,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  siteName: 'Payload Website Template',
 | 
			
		||||
  title: 'Payload Website Template',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const mergeOpenGraph = (og?: Metadata['openGraph']): Metadata['openGraph'] => {
 | 
			
		||||
  return {
 | 
			
		||||
    ...defaultOpenGraph,
 | 
			
		||||
    ...og,
 | 
			
		||||
    images: og?.images ? og.images : defaultOpenGraph.images,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
export const toKebabCase = (string: string): string =>
 | 
			
		||||
  string
 | 
			
		||||
    ?.replace(/([a-z])([A-Z])/g, '$1-$2')
 | 
			
		||||
    .replace(/\s+/g, '-')
 | 
			
		||||
    .toLowerCase()
 | 
			
		||||
@ -1,12 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Utility functions for UI components automatically added by ShadCN and used in a few of our frontend components and blocks.
 | 
			
		||||
 *
 | 
			
		||||
 * Other functions may be exported from here in the future or by installing other shadcn components.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { type ClassValue, clsx } from 'clsx'
 | 
			
		||||
import { twMerge } from 'tailwind-merge'
 | 
			
		||||
 | 
			
		||||
export function cn(...inputs: ClassValue[]) {
 | 
			
		||||
  return twMerge(clsx(inputs))
 | 
			
		||||
}
 | 
			
		||||
@ -1,108 +0,0 @@
 | 
			
		||||
'use client'
 | 
			
		||||
import type { RefObject } from 'react'
 | 
			
		||||
 | 
			
		||||
import { useRouter } from 'next/navigation'
 | 
			
		||||
import { useCallback, useEffect, useRef } from 'react'
 | 
			
		||||
 | 
			
		||||
type UseClickableCardType<T extends HTMLElement> = {
 | 
			
		||||
  card: {
 | 
			
		||||
    ref: RefObject<T | null>
 | 
			
		||||
  }
 | 
			
		||||
  link: {
 | 
			
		||||
    ref: RefObject<HTMLAnchorElement | null>
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  external?: boolean
 | 
			
		||||
  newTab?: boolean
 | 
			
		||||
  scroll?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useClickableCard<T extends HTMLElement>({
 | 
			
		||||
  external = false,
 | 
			
		||||
  newTab = false,
 | 
			
		||||
  scroll = true,
 | 
			
		||||
}: Props): UseClickableCardType<T> {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const card = useRef<T>(null)
 | 
			
		||||
  const link = useRef<HTMLAnchorElement>(null)
 | 
			
		||||
  const timeDown = useRef<number>(0)
 | 
			
		||||
  const hasActiveParent = useRef<boolean>(false)
 | 
			
		||||
  const pressedButton = useRef<number>(0)
 | 
			
		||||
 | 
			
		||||
  const handleMouseDown = useCallback(
 | 
			
		||||
    (e: MouseEvent) => {
 | 
			
		||||
      if (e.target) {
 | 
			
		||||
        const target = e.target as Element
 | 
			
		||||
 | 
			
		||||
        const timeNow = +new Date()
 | 
			
		||||
        const parent = target?.closest('a')
 | 
			
		||||
 | 
			
		||||
        pressedButton.current = e.button
 | 
			
		||||
 | 
			
		||||
        if (!parent) {
 | 
			
		||||
          hasActiveParent.current = false
 | 
			
		||||
          timeDown.current = timeNow
 | 
			
		||||
        } else {
 | 
			
		||||
          hasActiveParent.current = true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
    [router, card, link, timeDown],
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const handleMouseUp = useCallback(
 | 
			
		||||
    (e: MouseEvent) => {
 | 
			
		||||
      if (link.current?.href) {
 | 
			
		||||
        const timeNow = +new Date()
 | 
			
		||||
        const difference = timeNow - timeDown.current
 | 
			
		||||
 | 
			
		||||
        if (link.current?.href && difference <= 250) {
 | 
			
		||||
          if (!hasActiveParent.current && pressedButton.current === 0 && !e.ctrlKey) {
 | 
			
		||||
            if (external) {
 | 
			
		||||
              const target = newTab ? '_blank' : '_self'
 | 
			
		||||
              window.open(link.current.href, target)
 | 
			
		||||
            } else {
 | 
			
		||||
              router.push(link.current.href, { scroll })
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
    [router, card, link, timeDown],
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const cardNode = card.current
 | 
			
		||||
 | 
			
		||||
    const abortController = new AbortController()
 | 
			
		||||
 | 
			
		||||
    if (cardNode) {
 | 
			
		||||
      cardNode.addEventListener('mousedown', handleMouseDown, {
 | 
			
		||||
        signal: abortController.signal,
 | 
			
		||||
      })
 | 
			
		||||
      cardNode.addEventListener('mouseup', handleMouseUp, {
 | 
			
		||||
        signal: abortController.signal,
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      abortController.abort()
 | 
			
		||||
    }
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [card, link, router])
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    card: {
 | 
			
		||||
      ref: card,
 | 
			
		||||
    },
 | 
			
		||||
    link: {
 | 
			
		||||
      ref: link,
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default useClickableCard
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
import { useState, useEffect } from 'react'
 | 
			
		||||
 | 
			
		||||
export function useDebounce<T>(value: T, delay = 200): T {
 | 
			
		||||
  const [debouncedValue, setDebouncedValue] = useState<T>(value)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handler = setTimeout(() => {
 | 
			
		||||
      setDebouncedValue(value)
 | 
			
		||||
    }, delay)
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      clearTimeout(handler)
 | 
			
		||||
    }
 | 
			
		||||
  }, [value, delay])
 | 
			
		||||
 | 
			
		||||
  return debouncedValue
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user