├── .env.example ├── .gitignore ├── .npmrc ├── README.md ├── nodemon.json ├── package.json ├── src ├── access │ ├── isAdmin.ts │ ├── isAdminHasSiteAccessOrPublished.ts │ ├── isAdminOrHasSiteAccess.ts │ ├── isAdminOrSelf.ts │ └── isLoggedIn.ts ├── collections │ ├── ContactRequests.ts │ ├── Media.ts │ ├── Pages.ts │ ├── Sites.ts │ └── Users.ts ├── payload-types.ts ├── payload.config.ts ├── seed.ts └── server.ts ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URI=mongodb://localhost/access-control-demo 2 | PAYLOAD_SECRET=YOUR-SECRET-GOES-HERE -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional stylelint cache 59 | .stylelintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variable files 77 | .env 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | .env.local 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | .parcel-cache 86 | 87 | # Next.js build output 88 | .next 89 | out 90 | 91 | # Nuxt.js build / generate output 92 | .nuxt 93 | dist 94 | 95 | # Gatsby files 96 | .cache/ 97 | # Comment in the public line in if your project uses Gatsby and not Next.js 98 | # https://nextjs.org/blog/next-9-1#public-directory-support 99 | # public 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # vuepress v2.x temp and cache directory 105 | .temp 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | ### Node Patch ### 133 | # Serverless Webpack directories 134 | .webpack/ 135 | 136 | # Optional stylelint cache 137 | 138 | # SvelteKit build / generate output 139 | .svelte-kit 140 | 141 | ### VisualStudioCode ### 142 | .vscode/* 143 | !.vscode/settings.json 144 | !.vscode/tasks.json 145 | !.vscode/launch.json 146 | !.vscode/extensions.json 147 | !.vscode/*.code-snippets 148 | 149 | # Local History for Visual Studio Code 150 | .history/ 151 | 152 | # Built Visual Studio Code Extensions 153 | *.vsix 154 | 155 | ### VisualStudioCode Patch ### 156 | # Ignore all local history of files 157 | .history 158 | .ionide 159 | 160 | # Support for Project snippet scope 161 | .vscode/*.code-snippets 162 | 163 | # Ignore code-workspaces 164 | *.code-workspace 165 | 166 | # End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode 167 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Payload CMS Access Control Demo 2 | 3 | This repository contains an example that showcases how powerful Payload's access control is, and how it can be used to create simple, yet powerful editor experiences where you can control who can do what to a very fine-grained level. 4 | 5 | [Check out the video](https://www.youtube.com/watch?v=DoPLyXG26Dg) that corresponds with this repo. 6 | 7 | This is one of Payload's most important features—it's way more powerful than a typical RBAC pattern, which is often inflexible when it comes to document-based or even field-based permissions. With Payload's access control, you can build incredibly powerful and secure apps. 8 | 9 | Payload's admin UI automatically responds to the access control that you define. For example, if a user can't edit a document, the "Publish" button will be automatically removed. If a user can't edit a field, the field is automatically set to read-only in the admin UI. If a user can't create new documents in a collection, the "Create New" function will be disabled throughout all of the admin UI. 10 | 11 | **The best part is that it's all done in clean, declarative, easy-to-read code.** 12 | 13 | In this demo, we'll cover how to: 14 | 15 | - Implement and share access control that shows how to create a "multi-tenant"-style infrastructure on top of Payload 16 | - Utilize access control on the collection-level, to restrict who can do what on a collection basis 17 | - Utilize field-level access control to allow users to update some, but not all, fields on a given document 18 | - Open up a collection for "public" creation (in this case, we'll create a Contact Requests collection, which anyone can submit to) 19 | - Restrict who can access `draft` documents 20 | - Auto-generate and re-use TypeScript types based on the shape of your collections 21 | - Seed documents and set up a great developer environment for working locally 22 | 23 | ## Getting started 24 | 25 | To get started with this repo, do the following: 26 | 27 | - Clone the repo 28 | - Make an `.env` file by running `cp .env.example .env` in your project directory. Make sure to fill in your MongoDB connection string within this file if necessary 29 | - Run `yarn` or `npm install --legacy-peer-deps` 30 | - Run `yarn dev:seed` 31 | - Go to `http://localhost:3000` in your browser and check it out! 32 | 33 | #### Give us a star on GitHub 34 | 35 | > If you haven't already, stop by our GitHub page and [**leave us a star**](https://github.com/payloadcms/payload) by clicking on the star icon in the top right corner. This helps us grow and gain exposure within the development community. 36 | 37 | #### Join our community on Discord 38 | 39 | We've recently started a Discord community for the Payload community to interact in realtime. Often, Discord will be the first to hear about announcements like this move to open-source, and it can prove to be a great resource if you need help building with Payload. [Click here](https://discord.com/invite/r6sCXqVk3v) to join! 40 | 41 | #### Get up and running with one line 42 | 43 | Getting started is easy—and free forever. Just fire up a new terminal window and run the following command: 44 | 45 | ``` 46 | npx create-payload-app 47 | ``` 48 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts", 3 | "exec": "ts-node src/server.ts" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payload-access-control-demo", 3 | "description": "A demo of the powerful access control measures built into Payload", 4 | "version": "1.0.0", 5 | "main": "dist/server.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev:seed": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts PAYLOAD_DROP_DATABASE=true PAYLOAD_SEED=true nodemon", 9 | "dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon", 10 | "build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build", 11 | "build:server": "tsc", 12 | "build": "yarn copyfiles && yarn build:payload && yarn build:server", 13 | "serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js", 14 | "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/", 15 | "generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types", 16 | "generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema" 17 | }, 18 | "dependencies": { 19 | "dotenv": "^8.2.0", 20 | "express": "^4.17.1", 21 | "payload": "^1.1.9" 22 | }, 23 | "devDependencies": { 24 | "@types/express": "^4.17.9", 25 | "copyfiles": "^2.4.1", 26 | "cross-env": "^7.0.3", 27 | "nodemon": "^2.0.6", 28 | "ts-node": "^9.1.1", 29 | "typescript": "^4.1.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/access/isAdmin.ts: -------------------------------------------------------------------------------- 1 | import { Access, FieldAccess } from "payload/types"; 2 | import { User } from "../payload-types"; 3 | 4 | export const isAdmin: Access = ({ req: { user } }) => { 5 | // Return true or false based on if the user has an admin role 6 | return Boolean(user?.roles?.includes('admin')); 7 | } 8 | 9 | export const isAdminFieldLevel: FieldAccess<{ id: string }, unknown, User> = ({ req: { user } }) => { 10 | // Return true or false based on if the user has an admin role 11 | return Boolean(user?.roles?.includes('admin')); 12 | } -------------------------------------------------------------------------------- /src/access/isAdminHasSiteAccessOrPublished.ts: -------------------------------------------------------------------------------- 1 | import { Access } from "payload/config"; 2 | 3 | export const isAdminOrHasSiteAccessOrPublished: Access = ({ req: { user } }) => { 4 | // Need to be logged in 5 | if (user) { 6 | // If user has role of 'admin' 7 | if (user.roles.includes('admin')) return true; 8 | 9 | // If user has role of 'editor' and has access to a site, 10 | // return a query constraint to restrict the documents this user can edit 11 | // to only those that are assigned to a site, or have no site assigned 12 | if (user.roles.includes('editor') && user.sites?.length > 0) { 13 | return { 14 | or: [ 15 | { 16 | site: { 17 | in: user.sites 18 | } 19 | }, 20 | { 21 | site: { 22 | exists: false, 23 | } 24 | } 25 | ] 26 | } 27 | } 28 | } 29 | 30 | // Non-logged in users can only read published docs 31 | return { 32 | _status: { 33 | equals: 'published' 34 | } 35 | }; 36 | } -------------------------------------------------------------------------------- /src/access/isAdminOrHasSiteAccess.ts: -------------------------------------------------------------------------------- 1 | import { Access } from "payload/config"; 2 | 3 | export const isAdminOrHasSiteAccess = (siteIDFieldName: string = 'site'): Access => ({ req: { user } }) => { 4 | // Need to be logged in 5 | if (user) { 6 | // If user has role of 'admin' 7 | if (user.roles.includes('admin')) return true; 8 | 9 | // If user has role of 'editor' and has access to a site, 10 | // return a query constraint to restrict the documents this user can edit 11 | // to only those that are assigned to a site, or have no site assigned 12 | if (user.roles.includes('editor') && user.sites?.length > 0) { 13 | 14 | // Otherwise, we can restrict it based on the `site` field 15 | return { 16 | or: [ 17 | { 18 | [siteIDFieldName]: { 19 | in: user.sites 20 | } 21 | }, 22 | { 23 | [siteIDFieldName]: { 24 | exists: false, 25 | } 26 | } 27 | ] 28 | } 29 | } 30 | } 31 | 32 | // Reject everyone else 33 | return false; 34 | } 35 | -------------------------------------------------------------------------------- /src/access/isAdminOrSelf.ts: -------------------------------------------------------------------------------- 1 | import { Access } from "payload/config"; 2 | 3 | export const isAdminOrSelf: Access = ({ req: { user } }) => { 4 | // Need to be logged in 5 | if (user) { 6 | // If user has role of 'admin' 7 | if (user.roles?.includes('admin')) { 8 | return true; 9 | } 10 | 11 | // If any other type of user, only provide access to themselves 12 | return { 13 | id: { 14 | equals: user.id, 15 | } 16 | } 17 | } 18 | 19 | // Reject everyone else 20 | return false; 21 | } -------------------------------------------------------------------------------- /src/access/isLoggedIn.ts: -------------------------------------------------------------------------------- 1 | import { Access } from "payload/config"; 2 | import { User } from "../payload-types"; 3 | 4 | export const isLoggedIn: Access = ({ req: { user } }) => { 5 | // Return true if user is logged in, false if not 6 | return Boolean(user); 7 | } -------------------------------------------------------------------------------- /src/collections/ContactRequests.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | import { isAdmin } from '../access/isAdmin'; 3 | 4 | export const ContactRequests: CollectionConfig = { 5 | slug: 'contact-requests', 6 | access: { 7 | // Anyone can create, even unauthenticated 8 | create: () => true, 9 | // No one can update, ever 10 | update: () => false, 11 | // Only admins can read 12 | read: isAdmin, 13 | // No one can delete, ever 14 | delete: () => false, 15 | }, 16 | fields: [ 17 | { 18 | name: 'message', 19 | type: 'textarea', 20 | required: true, 21 | }, 22 | ], 23 | } -------------------------------------------------------------------------------- /src/collections/Media.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from "payload/types"; 2 | import { isAdmin } from "../access/isAdmin"; 3 | import { isAdminOrHasSiteAccess } from "../access/isAdminOrHasSiteAccess"; 4 | import { isLoggedIn } from "../access/isLoggedIn"; 5 | 6 | export const Media: CollectionConfig = { 7 | slug: 'media', 8 | upload: true, 9 | access: { 10 | // Anyone logged in can create 11 | create: isLoggedIn, 12 | // Only admins or editors with site access can update 13 | update: isAdminOrHasSiteAccess(), 14 | // Only admins or editors with site access can read 15 | read: isAdminOrHasSiteAccess(), 16 | // Only admins or editors with site access can delete 17 | delete: isAdminOrHasSiteAccess(), 18 | }, 19 | fields: [ 20 | { 21 | name: 'alt', 22 | type: 'text', 23 | required: true, 24 | }, 25 | { 26 | name: 'site', 27 | type: 'relationship', 28 | relationTo: 'sites', 29 | required: true, 30 | // If user is not admin, set the site by default 31 | // to the first site that they have access to 32 | defaultValue: ({ user }) => { 33 | if (!user.roles.includes('admin') && user.sites?.[0]) { 34 | return user.sites[0]; 35 | } 36 | } 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /src/collections/Pages.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | import { isAdmin } from '../access/isAdmin'; 3 | import { isAdminOrHasSiteAccessOrPublished } from '../access/isAdminHasSiteAccessOrPublished'; 4 | import { isAdminOrHasSiteAccess } from '../access/isAdminOrHasSiteAccess'; 5 | import { isLoggedIn } from '../access/isLoggedIn'; 6 | 7 | export const Pages: CollectionConfig = { 8 | slug: 'pages', 9 | admin: { 10 | useAsTitle: 'title', 11 | }, 12 | versions: { 13 | drafts: true, 14 | }, 15 | access: { 16 | // Anyone logged in can create 17 | create: isLoggedIn, 18 | // Only admins or editors with site access can update 19 | update: isAdminOrHasSiteAccess(), 20 | // Admins or editors with site access can read, 21 | // otherwise users not logged in can only read published 22 | read: isAdminOrHasSiteAccessOrPublished, 23 | // Only admins can delete 24 | delete: isAdmin, 25 | }, 26 | fields: [ 27 | { 28 | name: 'title', 29 | type: 'text', 30 | required: true, 31 | }, 32 | { 33 | name: 'content', 34 | type: 'richText', 35 | }, 36 | { 37 | name: 'site', 38 | type: 'relationship', 39 | relationTo: 'sites', 40 | required: true, 41 | // If user is not admin, set the site by default 42 | // to the first site that they have access to 43 | defaultValue: ({ user }) => { 44 | if (!user.roles.includes('admin') && user.sites?.[0]) { 45 | return user.sites[0]; 46 | } 47 | } 48 | } 49 | ], 50 | } -------------------------------------------------------------------------------- /src/collections/Sites.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from "payload/types"; 2 | import { isAdmin } from "../access/isAdmin"; 3 | import { isAdminOrHasSiteAccess } from "../access/isAdminOrHasSiteAccess"; 4 | 5 | export const Sites: CollectionConfig = { 6 | slug: 'sites', 7 | admin: { 8 | useAsTitle: 'title', 9 | }, 10 | access: { 11 | // Only admins can create 12 | create: isAdmin, 13 | // Only admins or editors with site access can read 14 | read: isAdminOrHasSiteAccess('id'), 15 | // Only admins can update 16 | update: isAdmin, 17 | // Only admins can delete 18 | delete: isAdmin, 19 | }, 20 | fields: [ 21 | { 22 | name: 'title', 23 | type: 'text', 24 | required: true, 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /src/collections/Users.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | import { isAdmin, isAdminFieldLevel } from '../access/isAdmin'; 3 | import { isAdminOrSelf } from '../access/isAdminOrSelf'; 4 | 5 | export const Users: CollectionConfig = { 6 | slug: 'users', 7 | auth: { 8 | // This property controls how deeply "populated" 9 | // relationship docs are that are stored in the req.user. 10 | // It should be kept to as low as possible, which 11 | // keeps performance fast. 12 | depth: 0, 13 | }, 14 | admin: { 15 | useAsTitle: 'email', 16 | }, 17 | access: { 18 | // Only admins can create users 19 | create: isAdmin, 20 | // Admins can read all, but any other logged in user can only read themselves 21 | read: isAdminOrSelf, 22 | // Admins can update all, but any other logged in user can only update themselves 23 | update: isAdminOrSelf, 24 | // Only admins can delete 25 | delete: isAdmin, 26 | }, 27 | fields: [ 28 | { 29 | type: 'row', 30 | fields: [ 31 | { 32 | name: 'firstName', 33 | type: 'text', 34 | required: true, 35 | }, 36 | { 37 | name: 'lastName', 38 | type: 'text', 39 | required: true, 40 | }, 41 | ], 42 | }, 43 | { 44 | name: 'roles', 45 | // Save this field to JWT so we can use from `req.user` 46 | saveToJWT: true, 47 | type: 'select', 48 | hasMany: true, 49 | defaultValue: ['editor'], 50 | access: { 51 | // Only admins can create or update a value for this field 52 | create: isAdminFieldLevel, 53 | update: isAdminFieldLevel, 54 | }, 55 | options: [ 56 | { 57 | label: 'Admin', 58 | value: 'admin', 59 | }, 60 | { 61 | label: 'Editor', 62 | value: 'editor', 63 | }, 64 | ] 65 | }, 66 | { 67 | name: 'sites', 68 | // Save this field to JWT so we can use from `req.user` 69 | saveToJWT: true, 70 | type: 'relationship', 71 | relationTo: 'sites', 72 | hasMany: true, 73 | access: { 74 | // Only admins can create or update a value for this field 75 | create: isAdminFieldLevel, 76 | update: isAdminFieldLevel, 77 | }, 78 | admin: { 79 | condition: ({ roles }) => roles && !roles.includes('admin'), 80 | description: 'This field sets which sites that this user has access to.' 81 | } 82 | } 83 | ], 84 | }; 85 | -------------------------------------------------------------------------------- /src/payload-types.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /** 3 | * This file was automatically generated by Payload CMS. 4 | * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, 5 | * and re-run `payload generate:types` to regenerate this file. 6 | */ 7 | 8 | export interface Config {} 9 | /** 10 | * This interface was referenced by `Config`'s JSON-Schema 11 | * via the `definition` "contact-requests". 12 | */ 13 | export interface ContactRequest { 14 | id: string; 15 | message: string; 16 | createdAt: string; 17 | updatedAt: string; 18 | } 19 | /** 20 | * This interface was referenced by `Config`'s JSON-Schema 21 | * via the `definition` "media". 22 | */ 23 | export interface Media { 24 | id: string; 25 | alt: string; 26 | site: string | Site; 27 | url?: string; 28 | filename?: string; 29 | mimeType?: string; 30 | filesize?: number; 31 | width?: number; 32 | height?: number; 33 | createdAt: string; 34 | updatedAt: string; 35 | } 36 | /** 37 | * This interface was referenced by `Config`'s JSON-Schema 38 | * via the `definition` "sites". 39 | */ 40 | export interface Site { 41 | id: string; 42 | title: string; 43 | createdAt: string; 44 | updatedAt: string; 45 | } 46 | /** 47 | * This interface was referenced by `Config`'s JSON-Schema 48 | * via the `definition` "pages". 49 | */ 50 | export interface Page { 51 | id: string; 52 | title: string; 53 | content?: { 54 | [k: string]: unknown; 55 | }[]; 56 | site: string | Site; 57 | _status?: 'draft' | 'published'; 58 | createdAt: string; 59 | updatedAt: string; 60 | } 61 | /** 62 | * This interface was referenced by `Config`'s JSON-Schema 63 | * via the `definition` "users". 64 | */ 65 | export interface User { 66 | id: string; 67 | firstName: string; 68 | lastName: string; 69 | roles?: ('admin' | 'editor')[]; 70 | sites?: string[] | Site[]; 71 | email?: string; 72 | resetPasswordToken?: string; 73 | resetPasswordExpiration?: string; 74 | loginAttempts?: number; 75 | lockUntil?: string; 76 | createdAt: string; 77 | updatedAt: string; 78 | } 79 | -------------------------------------------------------------------------------- /src/payload.config.ts: -------------------------------------------------------------------------------- 1 | import { buildConfig } from 'payload/config'; 2 | import path from 'path'; 3 | import { Users } from './collections/Users'; 4 | import { Sites } from './collections/Sites'; 5 | import { Media } from './collections/Media'; 6 | import { ContactRequests } from './collections/ContactRequests'; 7 | import { Pages } from './collections/Pages'; 8 | import { seed } from './seed'; 9 | 10 | export default buildConfig({ 11 | admin: { 12 | user: Users.slug, 13 | }, 14 | collections: [ 15 | ContactRequests, 16 | Media, 17 | Pages, 18 | Sites, 19 | Users, 20 | ], 21 | typescript: { 22 | outputFile: path.resolve(__dirname, 'payload-types.ts'), 23 | }, 24 | onInit: async (payload) => { 25 | // If the `env` var `PAYLOAD_SEED` is set, seed the db 26 | if (process.env.PAYLOAD_SEED) { 27 | await seed(payload); 28 | } 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/seed.ts: -------------------------------------------------------------------------------- 1 | import { Payload } from "payload"; 2 | import { User, Page, Site } from "./payload-types"; 3 | 4 | export const seed = async (payload: Payload): Promise => { 5 | const site1 = await payload.create({ 6 | collection: 'sites', 7 | data: { 8 | title: 'Site 1', 9 | } 10 | }); 11 | 12 | const site2 = await payload.create({ 13 | collection: 'sites', 14 | data: { 15 | title: 'Site 2', 16 | } 17 | }); 18 | 19 | // Local API methods skip all access control by default 20 | // so we can easily create an admin user directly in init 21 | await payload.create({ 22 | collection: 'users', 23 | data: { 24 | email: 'dev@payloadcms.com', 25 | password: 'test', 26 | firstName: 'Payload', 27 | lastName: 'CMS', 28 | roles: ['admin'] 29 | } 30 | }) 31 | 32 | // This user will be created with the default role of `editor` 33 | await payload.create({ 34 | collection: 'users', 35 | data: { 36 | email: 'site1@payloadcms.com', 37 | password: 'test', 38 | firstName: 'Site1', 39 | lastName: 'User', 40 | sites: [site1.id] 41 | } 42 | }) 43 | 44 | // This page will be created and assigned to Site 1 45 | await payload.create({ 46 | collection: 'pages', 47 | data: { 48 | _status: 'published', 49 | title: 'Site 1 Home', 50 | content: [ 51 | { 52 | children: [ 53 | { 54 | text: "Here's some content for Site 1's home page." 55 | } 56 | ] 57 | } 58 | ], 59 | site: site1.id 60 | } 61 | }) 62 | 63 | // This page will be created and assigned to Site 2 64 | await payload.create({ 65 | collection: 'pages', 66 | data: { 67 | _status: 'published', 68 | title: 'Site 2 Home', 69 | content: [ 70 | { 71 | children: [ 72 | { 73 | text: "Here's some content for Site 2's home page." 74 | } 75 | ] 76 | } 77 | ], 78 | site: site2.id 79 | } 80 | }) 81 | } -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import payload from 'payload'; 3 | 4 | require('dotenv').config(); 5 | const app = express(); 6 | 7 | // Redirect root to Admin panel 8 | app.get('/', (_, res) => { 9 | res.redirect('/admin'); 10 | }); 11 | 12 | const start = async () => { 13 | 14 | await payload.initAsync({ 15 | secret: process.env.PAYLOAD_SECRET, 16 | mongoURL: process.env.MONGODB_URI, 17 | express: app, 18 | onInit: () => { 19 | payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`) 20 | }, 21 | }) 22 | 23 | app.listen(3000); 24 | } 25 | 26 | start(); 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "strict": false, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "outDir": "./dist", 14 | "rootDir": "./src", 15 | "jsx": "react" 16 | }, 17 | "include": [ 18 | "src" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "dist", 23 | "build", 24 | ], 25 | "ts-node": { 26 | "transpileOnly": true 27 | } 28 | } 29 | --------------------------------------------------------------------------------