refact: init refact for mutitenent collections
This commit is contained in:
parent
b16a677e65
commit
9df88dbdda
141
package-lock.json
generated
141
package-lock.json
generated
@ -16,7 +16,9 @@
|
|||||||
"@payloadcms/db-postgres": "3.31.0",
|
"@payloadcms/db-postgres": "3.31.0",
|
||||||
"@payloadcms/next": "3.31.0",
|
"@payloadcms/next": "3.31.0",
|
||||||
"@payloadcms/payload-cloud": "3.31.0",
|
"@payloadcms/payload-cloud": "3.31.0",
|
||||||
|
"@payloadcms/plugin-multi-tenant": "^3.39.1",
|
||||||
"@payloadcms/richtext-lexical": "3.31.0",
|
"@payloadcms/richtext-lexical": "3.31.0",
|
||||||
|
"@payloadcms/ui": "^3.39.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.11",
|
"@radix-ui/react-dialog": "^1.1.11",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||||
"@radix-ui/react-label": "^2.1.4",
|
"@radix-ui/react-label": "^2.1.4",
|
||||||
@ -3952,6 +3954,55 @@
|
|||||||
"payload": "3.31.0"
|
"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": {
|
"node_modules/@payloadcms/payload-cloud": {
|
||||||
"version": "3.31.0",
|
"version": "3.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@payloadcms/payload-cloud/-/payload-cloud-3.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/@payloadcms/payload-cloud/-/payload-cloud-3.31.0.tgz",
|
||||||
@ -3970,6 +4021,17 @@
|
|||||||
"payload": "3.31.0"
|
"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": {
|
"node_modules/@payloadcms/richtext-lexical": {
|
||||||
"version": "3.31.0",
|
"version": "3.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@payloadcms/richtext-lexical/-/richtext-lexical-3.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/@payloadcms/richtext-lexical/-/richtext-lexical-3.31.0.tgz",
|
||||||
@ -4015,16 +4077,7 @@
|
|||||||
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
|
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@payloadcms/translations": {
|
"node_modules/@payloadcms/richtext-lexical/node_modules/@payloadcms/ui": {
|
||||||
"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",
|
"version": "3.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@payloadcms/ui/-/ui-3.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/@payloadcms/ui/-/ui-3.31.0.tgz",
|
||||||
"integrity": "sha512-SvRFqCmCo0PCOrwqFeNmL5EoJjGx7712l7pcvyMxpF0RmziZVAzqttnBizO3ha+7z65dJZFmyVHsawhO+iZk1Q==",
|
"integrity": "sha512-SvRFqCmCo0PCOrwqFeNmL5EoJjGx7712l7pcvyMxpF0RmziZVAzqttnBizO3ha+7z65dJZFmyVHsawhO+iZk1Q==",
|
||||||
@ -4063,6 +4116,74 @@
|
|||||||
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
|
"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": {
|
"node_modules/@payloadcms/ui/node_modules/sonner": {
|
||||||
"version": "1.7.4",
|
"version": "1.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
|
||||||
|
|||||||
@ -23,7 +23,9 @@
|
|||||||
"@payloadcms/db-postgres": "3.31.0",
|
"@payloadcms/db-postgres": "3.31.0",
|
||||||
"@payloadcms/next": "3.31.0",
|
"@payloadcms/next": "3.31.0",
|
||||||
"@payloadcms/payload-cloud": "3.31.0",
|
"@payloadcms/payload-cloud": "3.31.0",
|
||||||
|
"@payloadcms/plugin-multi-tenant": "^3.39.1",
|
||||||
"@payloadcms/richtext-lexical": "3.31.0",
|
"@payloadcms/richtext-lexical": "3.31.0",
|
||||||
|
"@payloadcms/ui": "^3.39.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.11",
|
"@radix-ui/react-dialog": "^1.1.11",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||||
"@radix-ui/react-label": "^2.1.4",
|
"@radix-ui/react-label": "^2.1.4",
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
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'
|
|
||||||
|
|
||||||
13
src/access/authenticatedOrPublished.ts
Normal file
13
src/access/authenticatedOrPublished.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { Access } from 'payload'
|
||||||
|
|
||||||
|
export const authenticatedOrPublished: Access = ({ req: { user } }) => {
|
||||||
|
if (user) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
_status: {
|
||||||
|
equals: 'published',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
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'))
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
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 { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||||
import { RscEntryLexicalField as RscEntryLexicalField_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'
|
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
@ -20,8 +22,12 @@ import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93
|
|||||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { BoldFeatureClient as BoldFeatureClient_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 { 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 = {
|
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#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
@ -43,5 +49,7 @@ export const importMap = {
|
|||||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864
|
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
"@payloadcms/plugin-multi-tenant/client#TenantSelector": TenantSelector_1d0591e3cf4f332c83a86da13a0de59a,
|
||||||
|
"@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { admin } from '@/access/admin'
|
//import { admin } from '@/access/admin'
|
||||||
import { authenticated } from '@/access/authenticated'
|
import { authenticated } from '@/access/authenticated'
|
||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
@ -12,9 +12,9 @@ export const Authors: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
read: () => true,
|
read: () => true,
|
||||||
update: admin,
|
// update: admin,
|
||||||
create: authenticated,
|
// create: authenticated,
|
||||||
delete: admin,
|
// delete: admin,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { admin } from "@/access/admin";
|
|
||||||
import { authenticated } from "@/access/authenticated";
|
import { authenticated } from "@/access/authenticated";
|
||||||
import { CollectionConfig } from "payload";
|
import { CollectionConfig } from "payload";
|
||||||
|
|
||||||
@ -9,9 +8,9 @@ export const Books: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
read: () => true,
|
read: () => true,
|
||||||
update: admin,
|
// update: admin,
|
||||||
create: authenticated,
|
// create: authenticated,
|
||||||
delete: admin,
|
// delete: admin,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { admin } from "@/access/admin";
|
|
||||||
import { CollectionConfig } from "payload";
|
import { CollectionConfig } from "payload";
|
||||||
|
|
||||||
export const Genre: CollectionConfig = {
|
export const Genre: CollectionConfig = {
|
||||||
@ -8,9 +7,9 @@ export const Genre: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
read: () => true,
|
read: () => true,
|
||||||
update: admin,
|
// update: admin,
|
||||||
create: () => true,
|
// create: () => true,
|
||||||
delete: admin,
|
// delete: admin,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { admin } from "@/access/admin";
|
|
||||||
import { authenticated } from "@/access/authenticated";
|
import { authenticated } from "@/access/authenticated";
|
||||||
import { CollectionConfig } from "payload";
|
import { CollectionConfig } from "payload";
|
||||||
|
|
||||||
@ -6,9 +5,9 @@ const Checkouts: CollectionConfig = {
|
|||||||
slug: 'checkouts',
|
slug: 'checkouts',
|
||||||
access: {
|
access: {
|
||||||
read: () => true,
|
read: () => true,
|
||||||
update: admin,
|
// update: admin,
|
||||||
create: authenticated,
|
// create: authenticated,
|
||||||
delete: admin,
|
// delete: admin,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { admin } from "@/access/admin";
|
|
||||||
import { authenticated } from "@/access/authenticated";
|
import { authenticated } from "@/access/authenticated";
|
||||||
import { CollectionConfig } from "payload";
|
import { CollectionConfig } from "payload";
|
||||||
|
|
||||||
@ -6,9 +5,9 @@ const HoldRequests: CollectionConfig = {
|
|||||||
slug: 'holdRequests',
|
slug: 'holdRequests',
|
||||||
access: {
|
access: {
|
||||||
read: () => true,
|
read: () => true,
|
||||||
update: admin,
|
// update: admin,
|
||||||
create: authenticated,
|
// create: authenticated,
|
||||||
delete: admin,
|
// delete: admin,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { admin } from "@/access/admin";
|
|
||||||
import { authenticated } from "@/access/authenticated";
|
import { authenticated } from "@/access/authenticated";
|
||||||
import { CollectionConfig } from "payload";
|
import { CollectionConfig } from "payload";
|
||||||
|
|
||||||
@ -9,9 +8,9 @@ export const Repositories: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
read: () => true,
|
read: () => true,
|
||||||
update: admin,
|
// update: admin,
|
||||||
create: authenticated,
|
// create: authenticated,
|
||||||
delete: admin,
|
// delete: admin,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|||||||
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'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/collections/Tenants/index.ts
Normal file
51
src/collections/Tenants/index.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
54
src/collections/Users/access/read.ts
Normal file
54
src/collections/Users/access/read.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
111
src/collections/Users/index.ts
Normal file
111
src/collections/Users/index.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
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],
|
||||||
|
},
|
||||||
|
}
|
||||||
27
src/hooks/formatSlug.ts
Normal file
27
src/hooks/formatSlug.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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
|
||||||
15
src/hooks/populatePublishedAt.ts
Normal file
15
src/hooks/populatePublishedAt.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
11
src/hooks/revalidateRedirects.ts
Normal file
11
src/hooks/revalidateRedirects.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
19
src/hooks/use-mobile.tsx
Normal file
19
src/hooks/use-mobile.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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,6 +1,7 @@
|
|||||||
// storage-adapter-import-placeholder
|
// storage-adapter-import-placeholder
|
||||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||||
|
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
|
||||||
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { buildConfig } from 'payload'
|
import { buildConfig } from 'payload'
|
||||||
@ -18,8 +19,12 @@ import { Header } from './globals/header/config'
|
|||||||
import { Pages } from './collections/Pages/Pages'
|
import { Pages } from './collections/Pages/Pages'
|
||||||
import HoldRequests from './collections/Checkouts/HoldRequests'
|
import HoldRequests from './collections/Checkouts/HoldRequests'
|
||||||
import Checkouts from './collections/Checkouts/Checkouts'
|
import Checkouts from './collections/Checkouts/Checkouts'
|
||||||
|
import { isSuperAdmin } from '@/access/isSuperAdmin'
|
||||||
|
|
||||||
import './envConfig.ts'
|
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 filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@ -40,7 +45,7 @@ export default buildConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
globals: [Header],
|
globals: [Header],
|
||||||
collections: [Users, Media, Books, Authors, Repositories, Copies, HoldRequests, Checkouts, Genre, Pages],
|
collections: [Tenants, Users, Media, Books, Authors, Repositories, Copies, HoldRequests, Checkouts, Genre, Pages],
|
||||||
editor: lexicalEditor(),
|
editor: lexicalEditor(),
|
||||||
secret: process.env.PAYLOAD_SECRET || '',
|
secret: process.env.PAYLOAD_SECRET || '',
|
||||||
typescript: {
|
typescript: {
|
||||||
@ -67,6 +72,33 @@ export default buildConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
//payloadCloudPlugin(),
|
//payloadCloudPlugin(),
|
||||||
// storage-adapter-placeholder
|
// 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: {
|
hooks: {
|
||||||
afterError: [
|
afterError: [
|
||||||
|
|||||||
1
src/utilities/canUseDOM.ts
Normal file
1
src/utilities/canUseDOM.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default !!(typeof window !== 'undefined' && window.document && window.document.createElement)
|
||||||
35
src/utilities/deepMerge.ts
Normal file
35
src/utilities/deepMerge.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
24
src/utilities/formatAuthors.ts
Normal file
24
src/utilities/formatAuthors.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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]}`
|
||||||
|
}
|
||||||
20
src/utilities/formatDateTime.ts
Normal file
20
src/utilities/formatDateTime.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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}`
|
||||||
|
}
|
||||||
49
src/utilities/generateMeta.ts
Normal file
49
src/utilities/generateMeta.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/utilities/generatePreviewPath.ts
Normal file
25
src/utilities/generatePreviewPath.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
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/getDocument.ts
Normal file
31
src/utilities/getDocument.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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}`],
|
||||||
|
})
|
||||||
26
src/utilities/getGlobals.ts
Normal file
26
src/utilities/getGlobals.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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}`],
|
||||||
|
})
|
||||||
43
src/utilities/getMeUser.ts
Normal file
43
src/utilities/getMeUser.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/utilities/getRedirects.ts
Normal file
26
src/utilities/getRedirects.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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'],
|
||||||
|
})
|
||||||
31
src/utilities/getURL.ts
Normal file
31
src/utilities/getURL.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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 || ''
|
||||||
|
}
|
||||||
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
|
||||||
|
}, []) || []
|
||||||
|
)
|
||||||
|
}
|
||||||
8
src/utilities/makeAcronym.ts
Normal file
8
src/utilities/makeAcronym.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const makeAcronym = (name: string, maxLength: number = 2) => {
|
||||||
|
return name
|
||||||
|
.split(' ')
|
||||||
|
.map((part) => part[0])
|
||||||
|
.slice(0, maxLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default makeAcronym
|
||||||
22
src/utilities/mergeOpenGraph.ts
Normal file
22
src/utilities/mergeOpenGraph.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/utilities/toKebabCase.ts
Normal file
5
src/utilities/toKebabCase.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const toKebabCase = (string: string): string =>
|
||||||
|
string
|
||||||
|
?.replace(/([a-z])([A-Z])/g, '$1-$2')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.toLowerCase()
|
||||||
12
src/utilities/ui.ts
Normal file
12
src/utilities/ui.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 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))
|
||||||
|
}
|
||||||
108
src/utilities/useClickableCard.ts
Normal file
108
src/utilities/useClickableCard.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
'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
|
||||||
17
src/utilities/useDebounce.ts
Normal file
17
src/utilities/useDebounce.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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