chore(examples): updates auth example (#3100)

This commit is contained in:
Jacob Fletcher
2023-08-01 16:53:31 -04:00
committed by GitHub
parent 62ecf54f85
commit b84496e5da
27 changed files with 41 additions and 17 deletions

View File

@@ -0,0 +1,7 @@
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3001
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
MONGODB_URI=mongodb://127.0.0.1/payload-example-auth
PAYLOAD_SECRET=PAYLOAD_AUTH_EXAMPLE_SECRET_KEY
COOKIE_DOMAIN=localhost
PAYLOAD_PUBLIC_SEED=true
PAYLOAD_DROP_DATABASE=true

View File

@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['@payloadcms'],
}

5
examples/auth/payload/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
build
dist
node_modules
package-lock.json
.env

View File

@@ -0,0 +1,8 @@
module.exports = {
printWidth: 100,
parser: "typescript",
semi: false,
singleQuote: true,
trailingComma: "all",
arrowParens: "avoid",
};

View File

@@ -0,0 +1,71 @@
# Payload Auth Example
The [Payload Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth) demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview). Follow the [Quick Start](#quick-start) to get up and running quickly. There are various fully working front-ends made explicitly for this example, including:
- [Next.js App Router](../next-app)
- [Next.js Pages Router](../next-pages)
Follow the instructions in each respective README to get started. If you are setting up authentication for another front-end, please consider contributing to this repo with your own example!
## Quick Start
To spin up this example locally, follow these steps:
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:3000/admin` to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
## How it works
The `users` collection exposes all [auth-related operations](https://payloadcms.com/docs/authentication/operations) needed to create a fully custom workflow on your front-end using the REST or GraphQL APIs, including:
- `Me`
- `Login`
- `Logout`
- `Refresh Token`
- `Verify Email`
- `Unlock`
- `Forgot Password`
- `Reset Password`
The [`cors`](https://payloadcms.com/docs/production/preventing-abuse#cross-origin-resource-sharing-cors), [`csrf`](https://payloadcms.com/docs/production/preventing-abuse#cross-site-request-forgery-csrf), and [`cookies`](https://payloadcms.com/docs/authentication/config#options) settings are also configured to ensure that the admin panel and front-end can communicate with each other securely.
### Access Control
Basic role-based access control is setup to determine what users can and cannot do based on their roles, which are:
- `admin`: They can access the Payload admin panel to manage your application. They can see all data and make all operations.
- `user`: They cannot access the Payload admin panel and have a limited access to operations based on their user.
A `beforeChange` field hook called `protectRoles` is placed on this to automatically populate `roles` with the `user` role when a new user is created. It also protects roles from being changed by non-admins.
## Development
To spin up this example locally, follow the [Quick Start](#quick-start).
### Seed
On boot, a seed script is included to create a user with email `demo@payloadcms.com`, password `demo`, the role `admin`.
> NOTICE: seeding the database is destructive because it drops your current database to populate a fresh one from the seed template. Only run this command if you are starting a new project or can afford to lose your current data.
## Production
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
1. First invoke the `payload build` script by running `yarn build` or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
1. Then run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory.
### Deployment
The easiest way to deploy your project is to use [Payload Cloud](https://payloadcms.com/new/import), a one-click hosting solution to deploy production-ready instances of your Payload apps directly from your GitHub repo. You can also deploy your app manually, check out the [deployment documentation](https://payloadcms.com/docs/production/deployment) for full details.
## Questions
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).

View File

@@ -0,0 +1,4 @@
{
"ext": "ts",
"exec": "ts-node src/server.ts"
}

View File

@@ -0,0 +1,45 @@
{
"name": "payload-example-auth",
"description": "Payload authentication example.",
"version": "1.0.0",
"main": "dist/server.js",
"license": "MIT",
"scripts": {
"dev": "cross-env PAYLOAD_PUBLIC_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"build:server": "tsc",
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
"lint": "eslint src",
"lint:fix": "eslint --fix --ext .ts,.tsx src"
},
"dependencies": {
"dotenv": "^8.2.0",
"express": "^4.17.1",
"payload": "latest"
},
"devDependencies": {
"@payloadcms/eslint-config": "^0.0.1",
"@types/express": "^4.17.9",
"@types/node": "18.11.3",
"@types/react": "18.0.21",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"eslint": "^8.19.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-filenames": "^1.3.2",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"nodemon": "^2.0.6",
"prettier": "^2.7.1",
"ts-node": "^9.1.1",
"typescript": "^4.8.4"
}
}

View File

@@ -0,0 +1,62 @@
import type { CollectionConfig } from 'payload/types'
import { admins } from './access/admins'
import adminsAndUser from './access/adminsAndUser'
import { anyone } from './access/anyone'
import { checkRole } from './access/checkRole'
import { loginAfterCreate } from './hooks/loginAfterCreate'
import { protectRoles } from './hooks/protectRoles'
export const Users: CollectionConfig = {
slug: 'users',
auth: {
tokenExpiration: 28800, // 8 hours
cookies: {
sameSite: 'none',
secure: true,
domain: process.env.COOKIE_DOMAIN,
},
},
admin: {
useAsTitle: 'email',
},
access: {
read: adminsAndUser,
create: anyone,
update: adminsAndUser,
delete: admins,
admin: ({ req: { user } }) => checkRole(['admin'], user),
},
hooks: {
afterChange: [loginAfterCreate],
},
fields: [
{
name: 'firstName',
type: 'text',
},
{
name: 'lastName',
type: 'text',
},
{
name: 'roles',
type: 'select',
hasMany: true,
saveToJWT: true,
hooks: {
beforeChange: [protectRoles],
},
options: [
{
label: 'Admin',
value: 'admin',
},
{
label: 'User',
value: 'user',
},
],
},
],
}

View File

@@ -0,0 +1,5 @@
import type { Access } from 'payload/config'
import { checkRole } from './checkRole'
export const admins: Access = ({ req: { user } }) => checkRole(['admin'], user)

View File

@@ -0,0 +1,19 @@
import type { Access } from 'payload/config'
import { checkRole } from './checkRole'
const adminsAndUser: Access = ({ req: { user } }) => {
if (user) {
if (checkRole(['admin'], user)) {
return true
}
return {
id: user.id,
}
}
return false
}
export default adminsAndUser

View File

@@ -0,0 +1,3 @@
import type { Access } from 'payload/config'
export const anyone: Access = () => true

View File

@@ -0,0 +1,16 @@
import type { User } from '../../payload-types'
export const checkRole = (allRoles: User['roles'] = [], user: User = undefined): boolean => {
if (user) {
if (
allRoles.some(role => {
return user?.roles?.some(individualRole => {
return individualRole === role
})
})
)
return true
}
return false
}

View File

@@ -0,0 +1,29 @@
import type { AfterChangeHook } from 'payload/dist/collections/config/types'
export const loginAfterCreate: AfterChangeHook = async ({
doc,
req,
req: { payload, body = {}, res },
operation,
}) => {
if (operation === 'create') {
const { email, password } = body
if (email && password) {
const { user, token } = await payload.login({
collection: 'users',
data: { email, password },
req,
res,
})
return {
...doc,
token,
user,
}
}
}
return doc
}

View File

@@ -0,0 +1,16 @@
import type { User } from 'payload/generated-types'
import type { FieldHook } from 'payload/types'
// ensure there is always a `user` role
// do not let non-admins change roles
export const protectRoles: FieldHook<User & { id: string }> = async ({ req, data }) => {
const isAdmin = req.user?.roles.includes('admin') || data.email === 'demo@payloadcms.com' // for the seed script
if (!isAdmin) {
return ['user']
}
const userRoles = new Set(data?.roles || [])
userRoles.add('user')
return [...userRoles]
}

View File

@@ -0,0 +1,17 @@
import React from 'react'
const BeforeLogin: React.FC = () => {
if (process.env.PAYLOAD_PUBLIC_SEED === 'true') {
return (
<p>
{'Log in with the email '}
<strong>demo@payloadcms.com</strong>
{' and the password '}
<strong>demo</strong>.
</p>
)
}
return null
}
export default BeforeLogin

View File

@@ -0,0 +1,30 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {
collections: {
users: User;
};
globals: {};
}
export interface User {
id: string;
firstName?: string;
lastName?: string;
roles?: ('admin' | 'user')[];
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
salt?: string;
hash?: string;
loginAttempts?: number;
lockUntil?: string;
password?: string;
}

View File

@@ -0,0 +1,25 @@
import path from 'path'
import { buildConfig } from 'payload/config'
import { Users } from './collections/Users'
import BeforeLogin from './components/BeforeLogin'
export default buildConfig({
collections: [Users],
admin: {
components: {
beforeLogin: [BeforeLogin],
},
},
cors: [
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
].filter(Boolean),
csrf: [
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
].filter(Boolean),
typescript: {
outputFile: path.resolve(__dirname, 'payload-types.ts'),
},
})

View File

@@ -0,0 +1,12 @@
import type { Payload } from 'payload'
export const seed = async (payload: Payload): Promise<void> => {
await payload.create({
collection: 'users',
data: {
email: 'demo@payloadcms.com',
password: 'demo',
roles: ['admin'],
},
})
}

View File

@@ -0,0 +1,36 @@
import express from 'express'
import path from 'path'
import payload from 'payload'
import { seed } from './seed'
// eslint-disable-next-line
require('dotenv').config({
path: path.resolve(__dirname, '../.env'),
})
const app = express()
app.get('/', (_, res) => {
res.redirect('/admin')
})
const start = async (): Promise<void> => {
await payload.init({
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URI,
express: app,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
},
})
if (process.env.PAYLOAD_PUBLIC_SEED === 'true') {
payload.logger.info('---- SEEDING DATABASE ----')
await seed(payload)
}
app.listen(3000)
}
start()

View File

@@ -0,0 +1,39 @@
{
"compilerOptions": {
"target": "es2019",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react",
"sourceMap": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"paths": {
"payload/generated-types": [
"./src/payload-types.ts"
],
"node_modules/*": [
"./node_modules/*"
]
},
},
"include": [
"src"
],
"exclude": [
"node_modules",
"dist",
"build",
],
"ts-node": {
"transpileOnly": true
}
}

File diff suppressed because it is too large Load Diff