chore(examples/auth): migrates to 3.0 (#5877)
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
# Payload Auth Example Front-End
|
||||
|
||||
This is a [Payload](https://payloadcms.com) + [Next.js](https://nextjs.org) app using the [App Router](https://nextjs.org/docs/app) made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth). It demonstrates how to authenticate your Next.js app using [Payload Authentication](https://payloadcms.com/docs/authentication/overview).
|
||||
This is a [Next.js](https://nextjs.org) [App Router](https://nextjs.org/docs/app) front-end made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth). This example demonstrates how to authenticate your Next.js app using [Payload Authentication](https://payloadcms.com/docs/authentication/overview).
|
||||
|
||||
> This example uses the App Router, the latest API of Next.js. If your app is using the legacy [Pages Router](https://nextjs.org/docs/pages), check out the official [Pages Router Example](https://github.com/payloadcms/payload/tree/main/examples/auth/next-pages).
|
||||
|
||||
**IMPORTANT—This application runs on a different server as Payload and establishes a connection from another domain or port over HTTP.** For an integrated setup that runs on a single server and uses the [Local API](https://payloadcms.com/docs/local-api/overview#local-api), check out [how to serve Payload alongside Next.js](https://github.com/payloadcms/payload/tree/main/examples/auth/payload). To learn more about this, check out [how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Payload
|
||||
@@ -13,9 +15,10 @@ First you'll need a running Payload app. There is one made explicitly for this e
|
||||
### Next.js
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
4. `pnpm dev`, `yarn dev`, or `npm run dev` to start the server
|
||||
5. `open http://localhost:3001` to see the result
|
||||
|
||||
Once running, a user is automatically seeded in your local environment with some basic instructions. See the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) for full details.
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function Home() {
|
||||
{". This example demonstrates how to implement Payload's "}
|
||||
<Link href="https://payloadcms.com/docs/authentication/overview">Authentication</Link>
|
||||
{
|
||||
' strategies in both the REST and GraphQL APIs. To toggle between these APIs, see `_layout.tsx`.'
|
||||
' strategies through http using the REST and GraphQL APIs. To toggle between these two APIs, see `_layout.tsx`.'
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -54,7 +54,7 @@ export const RecoverPasswordForm: React.FC = () => {
|
||||
<div className={classes.formWrapper}>
|
||||
<p>
|
||||
{`Please enter your email below. You will receive an email message with instructions on
|
||||
how to reset your password. To manage your all users, `}
|
||||
how to reset your password. To manage all of your users, `}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
|
||||
2765
examples/auth/next-app/pnpm-lock.yaml
generated
Normal file
2765
examples/auth/next-app/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,10 @@
|
||||
# Payload Auth Example Front-End
|
||||
|
||||
This is a [Payload](https://payloadcms.com) + [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages) made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth). It demonstrates how to authenticate your Next.js app using [Payload Authentication](https://payloadcms.com/docs/authentication/overview).
|
||||
This is a [Next.js](https://nextjs.org) [Pages Router](https://nextjs.org/docs/pages) front-end made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth). This example demonstrates how to authenticate your Next.js app using [Payload Authentication](https://payloadcms.com/docs/authentication/overview).
|
||||
|
||||
> This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/pages), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/main/examples/auth/next-app).
|
||||
> This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/app), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/main/examples/auth/next-app).
|
||||
|
||||
**IMPORTANT—This application runs on a different server as Payload and establishes a connection from another domain or port over HTTP.** For an integrated setup that runs on a single server and uses the [Local API](https://payloadcms.com/docs/local-api/overview#local-api), check out [how to serve Payload alongside Next.js](https://github.com/payloadcms/payload/tree/main/examples/auth/payload). To learn more about this, check out [how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -13,9 +15,10 @@ First you'll need a running Payload app. There is one made explicitly for this e
|
||||
### Next.js
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
4. `pnpm i`, `yarn dev`, or `npm run dev` to start the server
|
||||
5. `open http://localhost:3001` to see the result
|
||||
|
||||
Once running, a user is automatically seeded in your local environment with some basic instructions. See the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) for full details.
|
||||
|
||||
2287
examples/auth/next-pages/pnpm-lock.yaml
generated
Normal file
2287
examples/auth/next-pages/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ export default function Home() {
|
||||
{". This example demonstrates how to implement Payload's "}
|
||||
<Link href="https://payloadcms.com/docs/authentication/overview">Authentication</Link>
|
||||
{
|
||||
' strategies in both the REST and GraphQL APIs. To toggle between these APIs, see `_app.tsx`.'
|
||||
' strategies through HTTP using the REST and GraphQL APIs. To toggle between these two APIs, see `_app.tsx`.'
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -53,7 +53,7 @@ const RecoverPassword: React.FC = () => {
|
||||
<div className={classes.formWrapper}>
|
||||
<p>
|
||||
{`Please enter your email below. You will receive an email message with instructions on
|
||||
how to reset your password. To manage your all users, `}
|
||||
how to reset your password. To manage all of your users, `}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3001
|
||||
# NOTE: Change port of `PAYLOAD_PUBLIC_SITE_URL` if front-end is running on another server
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3000
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
DATABASE_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
|
||||
|
||||
12
examples/auth/payload/.eslintignore
Normal file
12
examples/auth/payload/.eslintignore
Normal file
@@ -0,0 +1,12 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
playwright.config.ts
|
||||
jest.config.js
|
||||
@@ -1,4 +1,15 @@
|
||||
module.exports = {
|
||||
extends: ['plugin:@next/next/core-web-vitals', '@payloadcms'],
|
||||
ignorePatterns: ['**/payload-types.ts'],
|
||||
overrides: [
|
||||
{
|
||||
extends: ['plugin:@typescript-eslint/disable-type-checked'],
|
||||
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
root: true,
|
||||
extends: ['@payloadcms'],
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,39 +1,103 @@
|
||||
# Payload Auth Example
|
||||
|
||||
The [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/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:
|
||||
This [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview) into all types of applications. Follow the [Quick Start](#quick-start) to get up and running quickly.
|
||||
|
||||
**IMPORTANT—This example includes a fully integrated Next.js App Router front-end that runs on the same server as Payload.** If you are working on an application running on an entirely separate server, there are various fully working, separately running 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!
|
||||
Those applications run directly alongside this one. 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!
|
||||
|
||||
To learn more about this, [check out how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
||||
|
||||
## 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`
|
||||
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
|
||||
1. `cp .env.example .env` to copy the example environment variables
|
||||
|
||||
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
|
||||
|
||||
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
- Press `y` when prompted to seed the database
|
||||
1. `open http://localhost:3000` to access the home page
|
||||
1. `open http://localhost:3000/admin` to access the admin panel
|
||||
- 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:
|
||||
### Collections
|
||||
|
||||
- `Me`
|
||||
- `Login`
|
||||
- `Logout`
|
||||
- `Refresh Token`
|
||||
- `Verify Email`
|
||||
- `Unlock`
|
||||
- `Forgot Password`
|
||||
- `Reset Password`
|
||||
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend this functionality.
|
||||
|
||||
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.
|
||||
- #### Users (Authentication)
|
||||
|
||||
Users are auth-enabled and encompass both admins and regular users based on the value of their `roles` field. Only `admin` users can access your admin panel to manage your content whereas `user` can authenticate on your front-end and access-controlled interfaces. See [Access Control](#access-control) for more details.
|
||||
|
||||
**Local API**
|
||||
|
||||
On the server, Payload provides all operations needed to authenticate users server-side using the Local API. In Next.js that might look something like this:
|
||||
|
||||
```ts
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { getPayloadHMR } from '@payloadcms/next'
|
||||
import config from '../../payload.config'
|
||||
|
||||
export default async function AccountPage({ searchParams }) {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayloadHMR({ config: configPromise })
|
||||
const { permissions, user } = await payload.auth({ headers })
|
||||
|
||||
if (!user) {
|
||||
redirect(
|
||||
`/login?error=${encodeURIComponent('You must be logged in to access your account.')}&redirect=/account`,
|
||||
)
|
||||
}
|
||||
|
||||
return ...
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP**
|
||||
|
||||
The `users` collection also opens an http-layer to expose all [auth-related operations](https://payloadcms.com/docs/authentication/operations) through the REST and GraphQL APIs, including:
|
||||
|
||||
- `Me`
|
||||
- `Login`
|
||||
- `Logout`
|
||||
- `Refresh Token`
|
||||
- `Verify Email`
|
||||
- `Unlock`
|
||||
- `Forgot Password`
|
||||
- `Reset Password`
|
||||
|
||||
This might look something like this:
|
||||
|
||||
```ts
|
||||
await fetch('/api/users/me', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
> NOTE: You can still use the HTTP APIs on the server if you don't have access to the Local API.
|
||||
|
||||
### Security
|
||||
|
||||
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 all configured to ensure that the admin panel and front-end can communicate with each other securely.
|
||||
|
||||
For additional help, see the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs.
|
||||
|
||||
### Access Control
|
||||
|
||||
@@ -50,7 +114,7 @@ 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`.
|
||||
On boot, a seed migration performed 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.
|
||||
|
||||
@@ -58,14 +122,17 @@ On boot, a seed script is included to create a user with email `demo@payloadcms.
|
||||
|
||||
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.
|
||||
1. First invoke the `payload build` script by running `pnpm build`, `yarn build`, or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
|
||||
1. Then run `pnpm serve`, `yarn serve`, or `npm run serve` to run Node.js 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.
|
||||
If you are using an integrated Next.js setup, the easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. Otherwise, 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).
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
5
examples/auth/payload/next-env.d.ts
vendored
Normal file
5
examples/auth/payload/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
8
examples/auth/payload/next.config.mjs
Normal file
8
examples/auth/payload/next.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
import { withPayload } from '@payloadcms/next/withPayload'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Your Next.js config here
|
||||
}
|
||||
|
||||
export default withPayload(nextConfig)
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"ext": "ts",
|
||||
"exec": "ts-node src/server.ts -- -I",
|
||||
"stdin": false
|
||||
}
|
||||
@@ -1,48 +1,43 @@
|
||||
{
|
||||
"name": "payload-example-auth",
|
||||
"description": "Payload authentication example.",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/server.js",
|
||||
"description": "Payload authentication example.",
|
||||
"license": "MIT",
|
||||
"main": "dist/server.js",
|
||||
"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",
|
||||
"build": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts NODE_OPTIONS=--no-deprecation next build",
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts && pnpm seed && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src",
|
||||
"payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload",
|
||||
"seed": "npm run payload migrate:fresh",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest"
|
||||
"@payloadcms/db-mongodb": "3.0.0-beta.24",
|
||||
"@payloadcms/next": "3.0.0-beta.24",
|
||||
"@payloadcms/richtext-slate": "3.0.0-beta.24",
|
||||
"@payloadcms/ui": "3.0.0-beta.24",
|
||||
"cross-env": "^7.0.3",
|
||||
"next": "14.3.0-canary.7",
|
||||
"payload": "3.0.0-beta.24",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.51.3"
|
||||
},
|
||||
"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"
|
||||
"@next/eslint-plugin-next": "^13.1.6",
|
||||
"@payloadcms/eslint-config": "^1.1.1",
|
||||
"@swc/core": "^1.4.14",
|
||||
"@swc/types": "^0.1.6",
|
||||
"@types/node": "^20.11.25",
|
||||
"@types/react": "^18.2.64",
|
||||
"@types/react-dom": "^18.2.21",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8.57.0",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "5.4.4"
|
||||
}
|
||||
}
|
||||
|
||||
6425
examples/auth/payload/pnpm-lock.yaml
generated
Normal file
6425
examples/auth/payload/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
@import '../../_css/type.scss';
|
||||
|
||||
.button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.label {
|
||||
@extend %label;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.appearance--primary {
|
||||
background-color: var(--theme-elevation-1000);
|
||||
color: var(--theme-elevation-0);
|
||||
}
|
||||
|
||||
.appearance--secondary {
|
||||
background-color: transparent;
|
||||
box-shadow: inset 0 0 0 1px var(--theme-elevation-1000);
|
||||
}
|
||||
|
||||
.appearance--default {
|
||||
padding: 0;
|
||||
color: var(--theme-text);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client'
|
||||
|
||||
import type { ElementType } from 'react';
|
||||
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type Props = {
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
el?: 'a' | 'button' | 'link'
|
||||
href?: string
|
||||
invert?: boolean
|
||||
label?: string
|
||||
newTab?: boolean
|
||||
onClick?: () => void
|
||||
type?: 'button' | 'submit'
|
||||
}
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
type = 'button',
|
||||
appearance,
|
||||
className: classNameFromProps,
|
||||
disabled,
|
||||
el: elFromProps = 'link',
|
||||
href,
|
||||
invert,
|
||||
label,
|
||||
newTab,
|
||||
onClick,
|
||||
}) => {
|
||||
let el = elFromProps
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
|
||||
const className = [
|
||||
classes.button,
|
||||
classNameFromProps,
|
||||
classes[`appearance--${appearance}`],
|
||||
invert && classes[`${appearance}--invert`],
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const content = (
|
||||
<div className={classes.content}>
|
||||
<span className={classes.label}>{label}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (onClick || type === 'submit') el = 'button'
|
||||
|
||||
if (el === 'link') {
|
||||
return (
|
||||
<Link className={className} href={href || ''} {...newTabProps} onClick={onClick}>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const Element: ElementType = el
|
||||
|
||||
return (
|
||||
<Element
|
||||
className={className}
|
||||
href={href}
|
||||
type={type}
|
||||
{...newTabProps}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{content}
|
||||
</Element>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
.gutter {
|
||||
max-width: 1920px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.gutterLeft {
|
||||
padding-left: var(--gutter-h);
|
||||
}
|
||||
|
||||
.gutterRight {
|
||||
padding-right: var(--gutter-h);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Ref } from 'react';
|
||||
|
||||
import React, { forwardRef } from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
left?: boolean
|
||||
ref?: Ref<HTMLDivElement>
|
||||
right?: boolean
|
||||
}
|
||||
|
||||
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const { children, className, left = true, right = true } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
classes.gutter,
|
||||
left && classes.gutterLeft,
|
||||
right && classes.gutterRight,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Gutter.displayName = 'Gutter'
|
||||
@@ -0,0 +1,20 @@
|
||||
@use '../../../_css/queries.scss' as *;
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: calc(var(--base) / 4) var(--base);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
opacity: 1;
|
||||
transition: opacity 100ms linear;
|
||||
visibility: visible;
|
||||
|
||||
> * {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hide {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import { useAuth } from '../../../_providers/Auth'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const HeaderNav: React.FC = () => {
|
||||
const { user } = useAuth()
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={[
|
||||
classes.nav,
|
||||
// fade the nav in on user load to avoid flash of content and layout shift
|
||||
// Vercel also does this in their own website header, see https://vercel.com
|
||||
user === undefined && classes.hide,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{user && (
|
||||
<React.Fragment>
|
||||
<Link href="/account">Account</Link>
|
||||
<Link href="/logout">Logout</Link>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{!user && (
|
||||
<React.Fragment>
|
||||
<Link href="/login">Login</Link>
|
||||
<Link href="/create-account">Create Account</Link>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@use '../../_css/queries.scss' as *;
|
||||
|
||||
.header {
|
||||
padding: var(--base) 0;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: calc(var(--base) / 2) var(--base);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
:global([data-theme="light"]) {
|
||||
.logo {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import { Gutter } from '../Gutter'
|
||||
import { HeaderNav } from './Nav'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<Gutter className={classes.wrap}>
|
||||
<Link className={classes.logo} href="/">
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-light.svg"
|
||||
/>
|
||||
<Image
|
||||
alt="Payload Logo"
|
||||
height={30}
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-dark.svg"
|
||||
width={150}
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
<HeaderNav />
|
||||
</Gutter>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
|
||||
import type { Permissions } from 'payload/auth'
|
||||
import type { PayloadRequestWithData } from 'payload/types'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useAuth } from '../../_providers/Auth'
|
||||
|
||||
export const HydrateClientUser: React.FC<{
|
||||
permissions: Permissions
|
||||
user: PayloadRequestWithData['user']
|
||||
}> = ({ permissions, user }) => {
|
||||
const { setPermissions, setUser } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
setUser(user)
|
||||
setPermissions(permissions)
|
||||
}, [user, permissions, setUser, setPermissions])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
@import '../../_css/common';
|
||||
|
||||
.inputWrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
font-family: system-ui;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background: none;
|
||||
background-color: var(--theme-elevation-100);
|
||||
color: var(--theme-elevation-1000);
|
||||
height: calc(var(--base) * 2);
|
||||
line-height: calc(var(--base) * 2);
|
||||
padding: 0 calc(var(--base) / 2);
|
||||
|
||||
&:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:-webkit-autofill,
|
||||
&:-webkit-autofill:hover,
|
||||
&:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: var(--theme-text);
|
||||
-webkit-box-shadow: 0 0 0px 1000px var(--theme-elevation-150) inset;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.input {
|
||||
background-color: var(--theme-elevation-150);
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: var(--theme-error-150);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 0;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
margin-bottom: calc(var(--base) / 2);
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
font-size: small;
|
||||
line-height: 1.25;
|
||||
margin-top: 4px;
|
||||
color: red;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
error: any
|
||||
label: string
|
||||
name: string
|
||||
register: UseFormRegister<FieldValues & any> // eslint-disable-line @typescript-eslint/no-redundant-type-constituents
|
||||
required?: boolean
|
||||
type?: 'email' | 'number' | 'password' | 'text'
|
||||
validate?: (value: string) => boolean | string
|
||||
}
|
||||
|
||||
export const Input: React.FC<Props> = ({
|
||||
name,
|
||||
type = 'text',
|
||||
error,
|
||||
label,
|
||||
register,
|
||||
required,
|
||||
validate,
|
||||
}) => {
|
||||
return (
|
||||
<div className={classes.inputWrap}>
|
||||
<label className={classes.label} htmlFor="name">
|
||||
{`${label} ${required ? '*' : ''}`}
|
||||
</label>
|
||||
<input
|
||||
className={[classes.input, error && classes.error].filter(Boolean).join(' ')}
|
||||
{...{ type }}
|
||||
{...register(name, {
|
||||
required,
|
||||
validate,
|
||||
...(type === 'email'
|
||||
? {
|
||||
pattern: {
|
||||
message: 'Please enter a valid email',
|
||||
value: /\S[^\s@]*@\S+\.\S+/,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})}
|
||||
/>
|
||||
{error && (
|
||||
<div className={classes.errorMessage}>
|
||||
{!error?.message && error?.type === 'required'
|
||||
? 'This field is required'
|
||||
: error?.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
@import '../../_css/common';
|
||||
|
||||
.message {
|
||||
padding: calc(var(--base) / 2) calc(var(--base) / 2);
|
||||
line-height: 1.25;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.default {
|
||||
background-color: var(--theme-elevation-100);
|
||||
color: var(--theme-elevation-1000);
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: var(--theme-warning-500);
|
||||
color: var(--theme-warning-900);
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: var(--theme-error-500);
|
||||
color: var(--theme-error-900);
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: var(--theme-success-500);
|
||||
color: var(--theme-success-900);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.default {
|
||||
background-color: var(--theme-elevation-900);
|
||||
color: var(--theme-elevation-100);
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--theme-warning-100);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--theme-error-100);
|
||||
}
|
||||
|
||||
.success {
|
||||
color: var(--theme-success-100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const Message: React.FC<{
|
||||
className?: string
|
||||
error?: React.ReactNode
|
||||
message?: React.ReactNode
|
||||
success?: React.ReactNode
|
||||
warning?: React.ReactNode
|
||||
}> = ({ className, error, message, success, warning }) => {
|
||||
const messageToRender = message || error || success || warning
|
||||
|
||||
if (messageToRender) {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
classes.message,
|
||||
className,
|
||||
error && classes.error,
|
||||
success && classes.success,
|
||||
warning && classes.warning,
|
||||
!error && !success && !warning && classes.default,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{messageToRender}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import { Message } from '../Message'
|
||||
|
||||
export const RenderParams: React.FC<{
|
||||
className?: string
|
||||
message?: string
|
||||
params?: string[]
|
||||
}> = ({ className, message, params = ['error', 'message', 'success'] }) => {
|
||||
const searchParams = useSearchParams()
|
||||
const paramValues = params.map(param => searchParams.get(param)).filter(Boolean)
|
||||
|
||||
if (paramValues.length) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{paramValues.map(paramValue => (
|
||||
<Message
|
||||
key={paramValue}
|
||||
message={(message || 'PARAM')?.replace('PARAM', paramValue || '')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.richText {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
import serialize from './serialize'
|
||||
|
||||
const RichText: React.FC<{ className?: string; content: any }> = ({ className, content }) => {
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
|
||||
{serialize(content)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RichText
|
||||
@@ -0,0 +1,92 @@
|
||||
import escapeHTML from 'escape-html'
|
||||
import React, { Fragment } from 'react'
|
||||
import { Text } from 'slate'
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
type Children = Leaf[]
|
||||
|
||||
type Leaf = {
|
||||
[key: string]: unknown
|
||||
children: Children
|
||||
type: string
|
||||
url?: string
|
||||
value?: {
|
||||
alt: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
const serialize = (children: Children): React.ReactNode[] =>
|
||||
children.map((node, i) => {
|
||||
if (Text.isText(node)) {
|
||||
let text = <span dangerouslySetInnerHTML={{ __html: escapeHTML(node.text) }} />
|
||||
|
||||
if (node.bold) {
|
||||
text = <strong key={i}>{text}</strong>
|
||||
}
|
||||
|
||||
if (node.code) {
|
||||
text = <code key={i}>{text}</code>
|
||||
}
|
||||
|
||||
if (node.italic) {
|
||||
text = <em key={i}>{text}</em>
|
||||
}
|
||||
|
||||
if (node.underline) {
|
||||
text = (
|
||||
<span key={i} style={{ textDecoration: 'underline' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (node.strikethrough) {
|
||||
text = (
|
||||
<span key={i} style={{ textDecoration: 'line-through' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return <Fragment key={i}>{text}</Fragment>
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case 'h1':
|
||||
return <h1 key={i}>{serialize(node.children)}</h1>
|
||||
case 'h2':
|
||||
return <h2 key={i}>{serialize(node.children)}</h2>
|
||||
case 'h3':
|
||||
return <h3 key={i}>{serialize(node.children)}</h3>
|
||||
case 'h4':
|
||||
return <h4 key={i}>{serialize(node.children)}</h4>
|
||||
case 'h5':
|
||||
return <h5 key={i}>{serialize(node.children)}</h5>
|
||||
case 'h6':
|
||||
return <h6 key={i}>{serialize(node.children)}</h6>
|
||||
case 'blockquote':
|
||||
return <blockquote key={i}>{serialize(node.children)}</blockquote>
|
||||
case 'ul':
|
||||
return <ul key={i}>{serialize(node.children)}</ul>
|
||||
case 'ol':
|
||||
return <ol key={i}>{serialize(node.children)}</ol>
|
||||
case 'li':
|
||||
return <li key={i}>{serialize(node.children)}</li>
|
||||
case 'link':
|
||||
return (
|
||||
<a href={escapeHTML(node.url)} key={i}>
|
||||
{serialize(node.children)}
|
||||
</a>
|
||||
)
|
||||
|
||||
default:
|
||||
return <p key={i}>{serialize(node.children)}</p>
|
||||
}
|
||||
})
|
||||
|
||||
export default serialize
|
||||
117
examples/auth/payload/src/app/(app)/_css/app.scss
Normal file
117
examples/auth/payload/src/app/(app)/_css/app.scss
Normal file
@@ -0,0 +1,117 @@
|
||||
@use './queries.scss' as *;
|
||||
@use './colors.scss' as *;
|
||||
@use './type.scss' as *;
|
||||
@import './theme.scss';
|
||||
|
||||
:root {
|
||||
--base: 24px;
|
||||
--font-body: system-ui;
|
||||
--font-mono: 'Roboto Mono', monospace;
|
||||
|
||||
--gutter-h: 180px;
|
||||
--block-padding: 120px;
|
||||
|
||||
@include large-break {
|
||||
--gutter-h: 144px;
|
||||
--block-padding: 96px;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
--gutter-h: 24px;
|
||||
--block-padding: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
@extend %body;
|
||||
background: var(--theme-bg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
margin: 0;
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--theme-success-500);
|
||||
color: var(--color-base-800);
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--theme-success-500);
|
||||
color: var(--color-base-800);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@extend %h1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@extend %h2;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@extend %h3;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@extend %h4;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@extend %h5;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@extend %h6;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: var(--base) 0;
|
||||
|
||||
@include mid-break {
|
||||
margin: calc(var(--base) * 0.75) 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: var(--base);
|
||||
margin: 0 0 var(--base);
|
||||
}
|
||||
|
||||
a {
|
||||
color: currentColor;
|
||||
|
||||
&:focus {
|
||||
opacity: 0.8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
83
examples/auth/payload/src/app/(app)/_css/colors.scss
Normal file
83
examples/auth/payload/src/app/(app)/_css/colors.scss
Normal file
@@ -0,0 +1,83 @@
|
||||
:root {
|
||||
--color-base-0: rgb(255, 255, 255);
|
||||
--color-base-50: rgb(245, 245, 245);
|
||||
--color-base-100: rgb(235, 235, 235);
|
||||
--color-base-150: rgb(221, 221, 221);
|
||||
--color-base-200: rgb(208, 208, 208);
|
||||
--color-base-250: rgb(195, 195, 195);
|
||||
--color-base-300: rgb(181, 181, 181);
|
||||
--color-base-350: rgb(168, 168, 168);
|
||||
--color-base-400: rgb(154, 154, 154);
|
||||
--color-base-450: rgb(141, 141, 141);
|
||||
--color-base-500: rgb(128, 128, 128);
|
||||
--color-base-550: rgb(114, 114, 114);
|
||||
--color-base-600: rgb(101, 101, 101);
|
||||
--color-base-650: rgb(87, 87, 87);
|
||||
--color-base-700: rgb(74, 74, 74);
|
||||
--color-base-750: rgb(60, 60, 60);
|
||||
--color-base-800: rgb(47, 47, 47);
|
||||
--color-base-850: rgb(34, 34, 34);
|
||||
--color-base-900: rgb(20, 20, 20);
|
||||
--color-base-950: rgb(7, 7, 7);
|
||||
--color-base-1000: rgb(0, 0, 0);
|
||||
|
||||
--color-success-50: rgb(247, 255, 251);
|
||||
--color-success-100: rgb(240, 255, 247);
|
||||
--color-success-150: rgb(232, 255, 243);
|
||||
--color-success-200: rgb(224, 255, 239);
|
||||
--color-success-250: rgb(217, 255, 235);
|
||||
--color-success-300: rgb(209, 255, 230);
|
||||
--color-success-350: rgb(201, 255, 226);
|
||||
--color-success-400: rgb(193, 255, 222);
|
||||
--color-success-450: rgb(186, 255, 218);
|
||||
--color-success-500: rgb(178, 255, 214);
|
||||
--color-success-550: rgb(160, 230, 193);
|
||||
--color-success-600: rgb(142, 204, 171);
|
||||
--color-success-650: rgb(125, 179, 150);
|
||||
--color-success-700: rgb(107, 153, 128);
|
||||
--color-success-750: rgb(89, 128, 107);
|
||||
--color-success-800: rgb(71, 102, 86);
|
||||
--color-success-850: rgb(53, 77, 64);
|
||||
--color-success-900: rgb(36, 51, 43);
|
||||
--color-success-950: rgb(18, 25, 21);
|
||||
|
||||
--color-warning-50: rgb(255, 255, 246);
|
||||
--color-warning-100: rgb(255, 255, 237);
|
||||
--color-warning-150: rgb(254, 255, 228);
|
||||
--color-warning-200: rgb(254, 255, 219);
|
||||
--color-warning-250: rgb(254, 255, 210);
|
||||
--color-warning-300: rgb(254, 255, 200);
|
||||
--color-warning-350: rgb(254, 255, 191);
|
||||
--color-warning-400: rgb(253, 255, 182);
|
||||
--color-warning-450: rgb(253, 255, 173);
|
||||
--color-warning-500: rgb(253, 255, 164);
|
||||
--color-warning-550: rgb(228, 230, 148);
|
||||
--color-warning-600: rgb(202, 204, 131);
|
||||
--color-warning-650: rgb(177, 179, 115);
|
||||
--color-warning-700: rgb(152, 153, 98);
|
||||
--color-warning-750: rgb(127, 128, 82);
|
||||
--color-warning-800: rgb(101, 102, 66);
|
||||
--color-warning-850: rgb(76, 77, 49);
|
||||
--color-warning-900: rgb(51, 51, 33);
|
||||
--color-warning-950: rgb(25, 25, 16);
|
||||
|
||||
--color-error-50: rgb(255, 241, 241);
|
||||
--color-error-100: rgb(255, 226, 228);
|
||||
--color-error-150: rgb(255, 212, 214);
|
||||
--color-error-200: rgb(255, 197, 200);
|
||||
--color-error-250: rgb(255, 183, 187);
|
||||
--color-error-300: rgb(255, 169, 173);
|
||||
--color-error-350: rgb(255, 154, 159);
|
||||
--color-error-400: rgb(255, 140, 145);
|
||||
--color-error-450: rgb(255, 125, 132);
|
||||
--color-error-500: rgb(255, 111, 118);
|
||||
--color-error-550: rgb(230, 100, 106);
|
||||
--color-error-600: rgb(204, 89, 94);
|
||||
--color-error-650: rgb(179, 78, 83);
|
||||
--color-error-700: rgb(153, 67, 71);
|
||||
--color-error-750: rgb(128, 56, 59);
|
||||
--color-error-800: rgb(102, 44, 47);
|
||||
--color-error-850: rgb(77, 33, 35);
|
||||
--color-error-900: rgb(51, 22, 24);
|
||||
--color-error-950: rgb(25, 11, 12);
|
||||
}
|
||||
1
examples/auth/payload/src/app/(app)/_css/common.scss
Normal file
1
examples/auth/payload/src/app/(app)/_css/common.scss
Normal file
@@ -0,0 +1 @@
|
||||
@forward './queries.scss';
|
||||
28
examples/auth/payload/src/app/(app)/_css/queries.scss
Normal file
28
examples/auth/payload/src/app/(app)/_css/queries.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
$breakpoint-xs-width: 400px;
|
||||
$breakpoint-s-width: 768px;
|
||||
$breakpoint-m-width: 1024px;
|
||||
$breakpoint-l-width: 1440px;
|
||||
|
||||
@mixin extra-small-break {
|
||||
@media (max-width: #{$breakpoint-xs-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin small-break {
|
||||
@media (max-width: #{$breakpoint-s-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin mid-break {
|
||||
@media (max-width: #{$breakpoint-m-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin large-break {
|
||||
@media (max-width: #{$breakpoint-l-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
241
examples/auth/payload/src/app/(app)/_css/theme.scss
Normal file
241
examples/auth/payload/src/app/(app)/_css/theme.scss
Normal file
@@ -0,0 +1,241 @@
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--theme-success-50: var(--color-success-50);
|
||||
--theme-success-100: var(--color-success-100);
|
||||
--theme-success-150: var(--color-success-150);
|
||||
--theme-success-200: var(--color-success-200);
|
||||
--theme-success-250: var(--color-success-250);
|
||||
--theme-success-300: var(--color-success-300);
|
||||
--theme-success-350: var(--color-success-350);
|
||||
--theme-success-400: var(--color-success-400);
|
||||
--theme-success-450: var(--color-success-450);
|
||||
--theme-success-500: var(--color-success-500);
|
||||
--theme-success-550: var(--color-success-550);
|
||||
--theme-success-600: var(--color-success-600);
|
||||
--theme-success-650: var(--color-success-650);
|
||||
--theme-success-700: var(--color-success-700);
|
||||
--theme-success-750: var(--color-success-750);
|
||||
--theme-success-800: var(--color-success-800);
|
||||
--theme-success-850: var(--color-success-850);
|
||||
--theme-success-900: var(--color-success-900);
|
||||
--theme-success-950: var(--color-success-950);
|
||||
|
||||
--theme-warning-50: var(--color-warning-50);
|
||||
--theme-warning-100: var(--color-warning-100);
|
||||
--theme-warning-150: var(--color-warning-150);
|
||||
--theme-warning-200: var(--color-warning-200);
|
||||
--theme-warning-250: var(--color-warning-250);
|
||||
--theme-warning-300: var(--color-warning-300);
|
||||
--theme-warning-350: var(--color-warning-350);
|
||||
--theme-warning-400: var(--color-warning-400);
|
||||
--theme-warning-450: var(--color-warning-450);
|
||||
--theme-warning-500: var(--color-warning-500);
|
||||
--theme-warning-550: var(--color-warning-550);
|
||||
--theme-warning-600: var(--color-warning-600);
|
||||
--theme-warning-650: var(--color-warning-650);
|
||||
--theme-warning-700: var(--color-warning-700);
|
||||
--theme-warning-750: var(--color-warning-750);
|
||||
--theme-warning-800: var(--color-warning-800);
|
||||
--theme-warning-850: var(--color-warning-850);
|
||||
--theme-warning-900: var(--color-warning-900);
|
||||
--theme-warning-950: var(--color-warning-950);
|
||||
|
||||
--theme-error-50: var(--color-error-50);
|
||||
--theme-error-100: var(--color-error-100);
|
||||
--theme-error-150: var(--color-error-150);
|
||||
--theme-error-200: var(--color-error-200);
|
||||
--theme-error-250: var(--color-error-250);
|
||||
--theme-error-300: var(--color-error-300);
|
||||
--theme-error-350: var(--color-error-350);
|
||||
--theme-error-400: var(--color-error-400);
|
||||
--theme-error-450: var(--color-error-450);
|
||||
--theme-error-500: var(--color-error-500);
|
||||
--theme-error-550: var(--color-error-550);
|
||||
--theme-error-600: var(--color-error-600);
|
||||
--theme-error-650: var(--color-error-650);
|
||||
--theme-error-700: var(--color-error-700);
|
||||
--theme-error-750: var(--color-error-750);
|
||||
--theme-error-800: var(--color-error-800);
|
||||
--theme-error-850: var(--color-error-850);
|
||||
--theme-error-900: var(--color-error-900);
|
||||
--theme-error-950: var(--color-error-950);
|
||||
|
||||
--theme-elevation-0: var(--color-base-0);
|
||||
--theme-elevation-50: var(--color-base-50);
|
||||
--theme-elevation-100: var(--color-base-100);
|
||||
--theme-elevation-150: var(--color-base-150);
|
||||
--theme-elevation-200: var(--color-base-200);
|
||||
--theme-elevation-250: var(--color-base-250);
|
||||
--theme-elevation-300: var(--color-base-300);
|
||||
--theme-elevation-350: var(--color-base-350);
|
||||
--theme-elevation-400: var(--color-base-400);
|
||||
--theme-elevation-450: var(--color-base-450);
|
||||
--theme-elevation-500: var(--color-base-500);
|
||||
--theme-elevation-550: var(--color-base-550);
|
||||
--theme-elevation-600: var(--color-base-600);
|
||||
--theme-elevation-650: var(--color-base-650);
|
||||
--theme-elevation-700: var(--color-base-700);
|
||||
--theme-elevation-750: var(--color-base-750);
|
||||
--theme-elevation-800: var(--color-base-800);
|
||||
--theme-elevation-850: var(--color-base-850);
|
||||
--theme-elevation-900: var(--color-base-900);
|
||||
--theme-elevation-950: var(--color-base-950);
|
||||
--theme-elevation-1000: var(--color-base-1000);
|
||||
|
||||
--theme-bg: var(--theme-elevation-0);
|
||||
--theme-input-bg: var(--theme-elevation-50);
|
||||
--theme-text: var(--theme-elevation-750);
|
||||
--theme-border-color: var(--theme-elevation-150);
|
||||
|
||||
color-scheme: light;
|
||||
color: var(--theme-text);
|
||||
|
||||
--highlight-default-bg-color: var(--theme-success-400);
|
||||
--highlight-default-text-color: var(--theme-text);
|
||||
|
||||
--highlight-danger-bg-color: var(--theme-error-150);
|
||||
--highlight-danger-text-color: var(--theme-text);
|
||||
}
|
||||
|
||||
h1 a,
|
||||
h2 a,
|
||||
h3 a,
|
||||
h4 a,
|
||||
h5 a,
|
||||
h6 a {
|
||||
color: var(--theme-elevation-750);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-elevation-800);
|
||||
}
|
||||
|
||||
&:visited {
|
||||
color: var(--theme-elevation-750);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-elevation-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--theme-elevation-0: var(--color-base-1000);
|
||||
--theme-elevation-50: var(--color-base-950);
|
||||
--theme-elevation-100: var(--color-base-900);
|
||||
--theme-elevation-150: var(--color-base-850);
|
||||
--theme-elevation-200: var(--color-base-800);
|
||||
--theme-elevation-250: var(--color-base-750);
|
||||
--theme-elevation-300: var(--color-base-700);
|
||||
--theme-elevation-350: var(--color-base-650);
|
||||
--theme-elevation-400: var(--color-base-600);
|
||||
--theme-elevation-450: var(--color-base-550);
|
||||
--theme-elevation-500: var(--color-base-500);
|
||||
--theme-elevation-550: var(--color-base-450);
|
||||
--theme-elevation-600: var(--color-base-400);
|
||||
--theme-elevation-650: var(--color-base-350);
|
||||
--theme-elevation-700: var(--color-base-300);
|
||||
--theme-elevation-750: var(--color-base-250);
|
||||
--theme-elevation-800: var(--color-base-200);
|
||||
--theme-elevation-850: var(--color-base-150);
|
||||
--theme-elevation-900: var(--color-base-100);
|
||||
--theme-elevation-950: var(--color-base-50);
|
||||
--theme-elevation-1000: var(--color-base-0);
|
||||
|
||||
--theme-success-50: var(--color-success-950);
|
||||
--theme-success-100: var(--color-success-900);
|
||||
--theme-success-150: var(--color-success-850);
|
||||
--theme-success-200: var(--color-success-800);
|
||||
--theme-success-250: var(--color-success-750);
|
||||
--theme-success-300: var(--color-success-700);
|
||||
--theme-success-350: var(--color-success-650);
|
||||
--theme-success-400: var(--color-success-600);
|
||||
--theme-success-450: var(--color-success-550);
|
||||
--theme-success-500: var(--color-success-500);
|
||||
--theme-success-550: var(--color-success-450);
|
||||
--theme-success-600: var(--color-success-400);
|
||||
--theme-success-650: var(--color-success-350);
|
||||
--theme-success-700: var(--color-success-300);
|
||||
--theme-success-750: var(--color-success-250);
|
||||
--theme-success-800: var(--color-success-200);
|
||||
--theme-success-850: var(--color-success-150);
|
||||
--theme-success-900: var(--color-success-100);
|
||||
--theme-success-950: var(--color-success-50);
|
||||
|
||||
--theme-warning-50: var(--color-warning-950);
|
||||
--theme-warning-100: var(--color-warning-900);
|
||||
--theme-warning-150: var(--color-warning-850);
|
||||
--theme-warning-200: var(--color-warning-800);
|
||||
--theme-warning-250: var(--color-warning-750);
|
||||
--theme-warning-300: var(--color-warning-700);
|
||||
--theme-warning-350: var(--color-warning-650);
|
||||
--theme-warning-400: var(--color-warning-600);
|
||||
--theme-warning-450: var(--color-warning-550);
|
||||
--theme-warning-500: var(--color-warning-500);
|
||||
--theme-warning-550: var(--color-warning-450);
|
||||
--theme-warning-600: var(--color-warning-400);
|
||||
--theme-warning-650: var(--color-warning-350);
|
||||
--theme-warning-700: var(--color-warning-300);
|
||||
--theme-warning-750: var(--color-warning-250);
|
||||
--theme-warning-800: var(--color-warning-200);
|
||||
--theme-warning-850: var(--color-warning-150);
|
||||
--theme-warning-900: var(--color-warning-100);
|
||||
--theme-warning-950: var(--color-warning-50);
|
||||
|
||||
--theme-error-50: var(--color-error-950);
|
||||
--theme-error-100: var(--color-error-900);
|
||||
--theme-error-150: var(--color-error-850);
|
||||
--theme-error-200: var(--color-error-800);
|
||||
--theme-error-250: var(--color-error-750);
|
||||
--theme-error-300: var(--color-error-700);
|
||||
--theme-error-350: var(--color-error-650);
|
||||
--theme-error-400: var(--color-error-600);
|
||||
--theme-error-450: var(--color-error-550);
|
||||
--theme-error-500: var(--color-error-500);
|
||||
--theme-error-550: var(--color-error-450);
|
||||
--theme-error-600: var(--color-error-400);
|
||||
--theme-error-650: var(--color-error-350);
|
||||
--theme-error-700: var(--color-error-300);
|
||||
--theme-error-750: var(--color-error-250);
|
||||
--theme-error-800: var(--color-error-200);
|
||||
--theme-error-850: var(--color-error-150);
|
||||
--theme-error-900: var(--color-error-100);
|
||||
--theme-error-950: var(--color-error-50);
|
||||
|
||||
--theme-bg: var(--theme-elevation-100);
|
||||
--theme-text: var(--theme-elevation-900);
|
||||
--theme-input-bg: var(--theme-elevation-150);
|
||||
--theme-border-color: var(--theme-elevation-250);
|
||||
|
||||
color-scheme: dark;
|
||||
color: var(--theme-text);
|
||||
|
||||
--highlight-default-bg-color: var(--theme-success-100);
|
||||
--highlight-default-text-color: var(--theme-success-600);
|
||||
|
||||
--highlight-danger-bg-color: var(--theme-error-100);
|
||||
--highlight-danger-text-color: var(--theme-error-550);
|
||||
}
|
||||
|
||||
h1 a,
|
||||
h2 a,
|
||||
h3 a,
|
||||
h4 a,
|
||||
h5 a,
|
||||
h6 a {
|
||||
color: var(--theme-success-600);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-success-400);
|
||||
}
|
||||
|
||||
&:visited {
|
||||
color: var(--theme-success-700);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-success-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
examples/auth/payload/src/app/(app)/_css/type.scss
Normal file
110
examples/auth/payload/src/app/(app)/_css/type.scss
Normal file
@@ -0,0 +1,110 @@
|
||||
@use 'queries' as *;
|
||||
|
||||
%h1,
|
||||
%h2,
|
||||
%h3,
|
||||
%h4,
|
||||
%h5,
|
||||
%h6 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
%h1 {
|
||||
margin: 40px 0;
|
||||
font-size: 64px;
|
||||
line-height: 70px;
|
||||
font-weight: bold;
|
||||
|
||||
@include mid-break {
|
||||
margin: 24px 0;
|
||||
font-size: 42px;
|
||||
line-height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
%h2 {
|
||||
margin: 28px 0;
|
||||
font-size: 48px;
|
||||
line-height: 54px;
|
||||
font-weight: bold;
|
||||
|
||||
@include mid-break {
|
||||
margin: 22px 0;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
%h3 {
|
||||
margin: 24px 0;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
font-weight: bold;
|
||||
|
||||
@include mid-break {
|
||||
margin: 20px 0;
|
||||
font-size: 26px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
%h4 {
|
||||
margin: 20px 0;
|
||||
font-size: 26px;
|
||||
line-height: 32px;
|
||||
font-weight: bold;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
%h5 {
|
||||
margin: 20px 0;
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
font-weight: bold;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
%h6 {
|
||||
margin: 20px 0;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
%body {
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
%large-body {
|
||||
font-size: 25px;
|
||||
line-height: 32px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
%label {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
text-transform: uppercase;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
34
examples/auth/payload/src/app/(app)/_providers/Auth/gql.ts
Normal file
34
examples/auth/payload/src/app/(app)/_providers/Auth/gql.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export const USER = `
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
`
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const gql = async (query: string): Promise<any> => {
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
}),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const { data, errors } = await res.json()
|
||||
|
||||
if (errors) {
|
||||
throw new Error(errors[0].message)
|
||||
}
|
||||
|
||||
if (res.ok && data) {
|
||||
return data
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
186
examples/auth/payload/src/app/(app)/_providers/Auth/index.tsx
Normal file
186
examples/auth/payload/src/app/(app)/_providers/Auth/index.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
'use client'
|
||||
|
||||
import type { Permissions } from 'payload/auth'
|
||||
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import type { User } from '../../../../payload-types'
|
||||
import type { AuthContext, Create, ForgotPassword, Login, Logout, ResetPassword } from './types'
|
||||
|
||||
import { USER, gql } from './gql'
|
||||
import { rest } from './rest'
|
||||
|
||||
const Context = createContext({} as AuthContext)
|
||||
|
||||
export const AuthProvider: React.FC<{ api?: 'gql' | 'rest'; children: React.ReactNode }> = ({
|
||||
api = 'rest',
|
||||
children,
|
||||
}) => {
|
||||
const [user, setUser] = useState<User | null>()
|
||||
const [permissions, setPermissions] = useState<Permissions | null>(null)
|
||||
|
||||
const create = useCallback<Create>(
|
||||
async (args) => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users`, args)
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { createUser: user } = await gql(`mutation {
|
||||
createUser(data: { email: "${args.email}", password: "${args.password}", firstName: "${args.firstName}", lastName: "${args.lastName}" }) {
|
||||
${USER}
|
||||
}
|
||||
}`)
|
||||
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
},
|
||||
[api],
|
||||
)
|
||||
|
||||
const login = useCallback<Login>(
|
||||
async (args) => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/login`, args)
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { loginUser } = await gql(`mutation {
|
||||
loginUser(email: "${args.email}", password: "${args.password}") {
|
||||
user {
|
||||
${USER}
|
||||
}
|
||||
exp
|
||||
}
|
||||
}`)
|
||||
|
||||
setUser(loginUser?.user)
|
||||
return loginUser?.user
|
||||
}
|
||||
},
|
||||
[api],
|
||||
)
|
||||
|
||||
const logout = useCallback<Logout>(async () => {
|
||||
if (api === 'rest') {
|
||||
await rest(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/logout`)
|
||||
setUser(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
await gql(`mutation {
|
||||
logoutUser
|
||||
}`)
|
||||
|
||||
setUser(null)
|
||||
}
|
||||
}, [api])
|
||||
|
||||
// On mount, get user and set
|
||||
useEffect(() => {
|
||||
const fetchMe = async () => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/me`,
|
||||
{},
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
setUser(user)
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { meUser } = await gql(`query {
|
||||
meUser {
|
||||
user {
|
||||
${USER}
|
||||
}
|
||||
exp
|
||||
}
|
||||
}`)
|
||||
|
||||
setUser(meUser.user)
|
||||
}
|
||||
}
|
||||
|
||||
void fetchMe()
|
||||
}, [api])
|
||||
|
||||
const forgotPassword = useCallback<ForgotPassword>(
|
||||
async (args) => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/forgot-password`,
|
||||
args,
|
||||
)
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { forgotPasswordUser } = await gql(`mutation {
|
||||
forgotPasswordUser(email: "${args.email}")
|
||||
}`)
|
||||
|
||||
return forgotPasswordUser
|
||||
}
|
||||
},
|
||||
[api],
|
||||
)
|
||||
|
||||
const resetPassword = useCallback<ResetPassword>(
|
||||
async (args) => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/reset-password`,
|
||||
args,
|
||||
)
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { resetPasswordUser } = await gql(`mutation {
|
||||
resetPasswordUser(password: "${args.password}", token: "${args.token}") {
|
||||
user {
|
||||
${USER}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
setUser(resetPasswordUser.user)
|
||||
return resetPasswordUser.user
|
||||
}
|
||||
},
|
||||
[api],
|
||||
)
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
value={{
|
||||
create,
|
||||
forgotPassword,
|
||||
login,
|
||||
logout,
|
||||
permissions,
|
||||
resetPassword,
|
||||
setPermissions,
|
||||
setUser,
|
||||
user,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
type UseAuth<T = User> = () => AuthContext // eslint-disable-line no-unused-vars
|
||||
|
||||
export const useAuth: UseAuth = () => useContext(Context)
|
||||
34
examples/auth/payload/src/app/(app)/_providers/Auth/rest.ts
Normal file
34
examples/auth/payload/src/app/(app)/_providers/Auth/rest.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { User } from '../../../../payload-types'
|
||||
|
||||
export const rest = async (
|
||||
url: string,
|
||||
args?: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
options?: RequestInit,
|
||||
): Promise<User | null | undefined> => {
|
||||
const method = options?.method || 'POST'
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
...(method === 'POST' ? { body: JSON.stringify(args) } : {}),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
...options,
|
||||
})
|
||||
|
||||
const { errors, user } = await res.json()
|
||||
|
||||
if (errors) {
|
||||
throw new Error(errors[0].message)
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
return user
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
35
examples/auth/payload/src/app/(app)/_providers/Auth/types.ts
Normal file
35
examples/auth/payload/src/app/(app)/_providers/Auth/types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Permissions } from 'payload/auth'
|
||||
|
||||
import type { User } from '../../../../payload-types'
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export type ResetPassword = (args: {
|
||||
password: string
|
||||
passwordConfirm: string
|
||||
token: string
|
||||
}) => Promise<User>
|
||||
|
||||
export type ForgotPassword = (args: { email: string }) => Promise<User> // eslint-disable-line no-unused-vars
|
||||
|
||||
export type Create = (args: {
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
password: string
|
||||
}) => Promise<User> // eslint-disable-line no-unused-vars
|
||||
|
||||
export type Login = (args: { email: string; password: string }) => Promise<User> // eslint-disable-line no-unused-vars
|
||||
|
||||
export type Logout = () => Promise<void>
|
||||
|
||||
export interface AuthContext {
|
||||
create: Create
|
||||
forgotPassword: ForgotPassword
|
||||
login: Login
|
||||
logout: Logout
|
||||
permissions?: Permissions | null
|
||||
resetPassword: ResetPassword
|
||||
setPermissions: (permissions: Permissions | null) => void
|
||||
setUser: (user: User | null) => void // eslint-disable-line no-unused-vars
|
||||
user?: User | null
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.form {
|
||||
margin-bottom: var(--base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--base) / 2);
|
||||
align-items: flex-start;
|
||||
width: 66.66%;
|
||||
|
||||
@include mid-break {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.changePassword {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: calc(var(--base) / 2);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { Button } from '../../_components/Button'
|
||||
import { Input } from '../../_components/Input'
|
||||
import { Message } from '../../_components/Message'
|
||||
import { useAuth } from '../../_providers/Auth'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
name: string
|
||||
password: string
|
||||
passwordConfirm: string
|
||||
}
|
||||
|
||||
export const AccountForm: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
const { setUser, user } = useAuth()
|
||||
const [changePassword, setChangePassword] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
formState: { errors, isLoading },
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
watch,
|
||||
} = useForm<FormData>()
|
||||
|
||||
const password = useRef({})
|
||||
password.current = watch('password', '')
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
if (user) {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/${user.id}`, {
|
||||
// Make sure to include cookies with fetch
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'PATCH',
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json()
|
||||
setUser(json.doc)
|
||||
setSuccess('Successfully updated account.')
|
||||
setError('')
|
||||
setChangePassword(false)
|
||||
reset({
|
||||
name: json.doc.name,
|
||||
email: json.doc.email,
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
})
|
||||
} else {
|
||||
setError('There was a problem updating your account.')
|
||||
}
|
||||
}
|
||||
},
|
||||
[user, setUser, reset],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (user === null) {
|
||||
router.push(`/login?unauthorized=account`)
|
||||
}
|
||||
|
||||
// Once user is loaded, reset form to have default values
|
||||
if (user) {
|
||||
reset({
|
||||
email: user.email,
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
})
|
||||
}
|
||||
}, [user, router, reset, changePassword])
|
||||
|
||||
return (
|
||||
<form className={classes.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<Message className={classes.message} error={error} success={success} />
|
||||
{!changePassword ? (
|
||||
<Fragment>
|
||||
<p>
|
||||
{'To change your password, '}
|
||||
<button
|
||||
className={classes.changePassword}
|
||||
onClick={() => setChangePassword(!changePassword)}
|
||||
type="button"
|
||||
>
|
||||
click here
|
||||
</button>
|
||||
.
|
||||
</p>
|
||||
<Input
|
||||
error={errors.email}
|
||||
label="Email Address"
|
||||
name="email"
|
||||
register={register}
|
||||
required
|
||||
type="email"
|
||||
/>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<p>
|
||||
{'Change your password below, or '}
|
||||
<button
|
||||
className={classes.changePassword}
|
||||
onClick={() => setChangePassword(!changePassword)}
|
||||
type="button"
|
||||
>
|
||||
cancel
|
||||
</button>
|
||||
.
|
||||
</p>
|
||||
<Input
|
||||
error={errors.password}
|
||||
label="Password"
|
||||
name="password"
|
||||
register={register}
|
||||
required
|
||||
type="password"
|
||||
/>
|
||||
<Input
|
||||
error={errors.passwordConfirm}
|
||||
label="Confirm Password"
|
||||
name="passwordConfirm"
|
||||
register={register}
|
||||
required
|
||||
type="password"
|
||||
validate={value => value === password.current || 'The passwords do not match'}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={classes.submit}
|
||||
label={isLoading ? 'Processing' : changePassword ? 'Change password' : 'Update account'}
|
||||
type="submit"
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.account {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
|
||||
.params {
|
||||
margin-top: var(--base);
|
||||
}
|
||||
44
examples/auth/payload/src/app/(app)/account/page.tsx
Normal file
44
examples/auth/payload/src/app/(app)/account/page.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import Link from 'next/link'
|
||||
import { redirect } from 'next/navigation'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import config from '../../../payload.config'
|
||||
import { Button } from '../_components/Button'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { HydrateClientUser } from '../_components/HydrateClientUser'
|
||||
import { RenderParams } from '../_components/RenderParams'
|
||||
import { AccountForm } from './AccountForm'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function Account() {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayloadHMR({ config })
|
||||
const { permissions, user } = await payload.auth({ headers })
|
||||
|
||||
if (!user) {
|
||||
redirect(
|
||||
`/login?error=${encodeURIComponent('You must be logged in to access your account.')}&redirect=/account`,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<HydrateClientUser permissions={permissions} user={user} />
|
||||
<Gutter className={classes.account}>
|
||||
<RenderParams className={classes.params} />
|
||||
<h1>Account</h1>
|
||||
<p>
|
||||
{`This is your account dashboard. Here you can update your account information and more. To manage all users, `}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_SERVER_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<AccountForm />
|
||||
<Button appearance="secondary" href="/logout" label="Log out" />
|
||||
</Gutter>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.form {
|
||||
margin-bottom: var(--base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--base) / 2);
|
||||
align-items: flex-start;
|
||||
width: 66.66%;
|
||||
|
||||
@include mid-break {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: calc(var(--base) / 2);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: var(--base);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { Button } from '../../_components/Button'
|
||||
import { Input } from '../../_components/Input'
|
||||
import { Message } from '../../_components/Message'
|
||||
import { useAuth } from '../../_providers/Auth'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
password: string
|
||||
passwordConfirm: string
|
||||
}
|
||||
|
||||
export const CreateAccountForm: React.FC = () => {
|
||||
const searchParams = useSearchParams()
|
||||
const allParams = searchParams.toString() ? `?${searchParams.toString()}` : ''
|
||||
const { login } = useAuth()
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<null | string>(null)
|
||||
|
||||
const {
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
register,
|
||||
watch,
|
||||
} = useForm<FormData>()
|
||||
|
||||
const password = useRef({})
|
||||
password.current = watch('password', '')
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users`, {
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const message = response.statusText || 'There was an error creating the account.'
|
||||
setError(message)
|
||||
return
|
||||
}
|
||||
|
||||
const redirect = searchParams.get('redirect')
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setLoading(true)
|
||||
}, 1000)
|
||||
|
||||
try {
|
||||
await login(data)
|
||||
clearTimeout(timer)
|
||||
if (redirect) router.push(redirect)
|
||||
else router.push(`/account?success=${encodeURIComponent('Account created successfully')}`)
|
||||
} catch (_) {
|
||||
clearTimeout(timer)
|
||||
setError('There was an error with the credentials provided. Please try again.')
|
||||
}
|
||||
},
|
||||
[login, router, searchParams],
|
||||
)
|
||||
|
||||
return (
|
||||
<form className={classes.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<p>
|
||||
{`This is where new customers can signup and create a new account. To manage all users, `}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_SERVER_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<Message className={classes.message} error={error} />
|
||||
<Input
|
||||
error={errors.email}
|
||||
label="Email Address"
|
||||
name="email"
|
||||
register={register}
|
||||
required
|
||||
type="email"
|
||||
/>
|
||||
<Input
|
||||
error={errors.password}
|
||||
label="Password"
|
||||
name="password"
|
||||
register={register}
|
||||
required
|
||||
type="password"
|
||||
/>
|
||||
<Input
|
||||
error={errors.passwordConfirm}
|
||||
label="Confirm Password"
|
||||
name="passwordConfirm"
|
||||
register={register}
|
||||
required
|
||||
type="password"
|
||||
validate={value => value === password.current || 'The passwords do not match'}
|
||||
/>
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={classes.submit}
|
||||
label={loading ? 'Processing' : 'Create Account'}
|
||||
type="submit"
|
||||
/>
|
||||
<div>
|
||||
{'Already have an account? '}
|
||||
<Link href={`/login${allParams}`}>Login</Link>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@import "../_css/common";
|
||||
|
||||
.createAccount {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
32
examples/auth/payload/src/app/(app)/create-account/page.tsx
Normal file
32
examples/auth/payload/src/app/(app)/create-account/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { redirect } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import config from '../../../payload.config'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { RenderParams } from '../_components/RenderParams'
|
||||
import { CreateAccountForm } from './CreateAccountForm'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function CreateAccount() {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayloadHMR({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
if (user) {
|
||||
redirect(
|
||||
`/account?message=${encodeURIComponent(
|
||||
'Cannot create a new account while logged in, please log out and try again.',
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Gutter className={classes.createAccount}>
|
||||
<h1>Create Account</h1>
|
||||
<RenderParams />
|
||||
<CreateAccountForm />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
29
examples/auth/payload/src/app/(app)/layout.tsx
Normal file
29
examples/auth/payload/src/app/(app)/layout.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Header } from './_components/Header'
|
||||
import './_css/app.scss'
|
||||
import { AuthProvider } from './_providers/Auth'
|
||||
|
||||
export const metadata = {
|
||||
description: 'An example of how to authenticate with Payload from a Next.js app.',
|
||||
title: 'Payload Auth + Next.js App Router Example',
|
||||
}
|
||||
|
||||
export default function RootLayout(props: { children: React.ReactNode }) {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<AuthProvider
|
||||
// To toggle between the REST and GraphQL APIs,
|
||||
// change the `api` prop to either `rest` or `gql`
|
||||
api="rest" // change this to `gql` to use the GraphQL API
|
||||
>
|
||||
<Header />
|
||||
<main>{children}</main>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.form {
|
||||
margin-bottom: var(--base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--base) / 2);
|
||||
align-items: flex-start;
|
||||
width: 66.66%;
|
||||
|
||||
@include mid-break {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: calc(var(--base) / 2);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: var(--base);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { Button } from '../../_components/Button'
|
||||
import { Input } from '../../_components/Input'
|
||||
import { Message } from '../../_components/Message'
|
||||
import { useAuth } from '../../_providers/Auth'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export const LoginForm: React.FC = () => {
|
||||
const searchParams = useSearchParams()
|
||||
const allParams = searchParams.toString() ? `?${searchParams.toString()}` : ''
|
||||
const redirect = useRef(searchParams.get('redirect'))
|
||||
const { login } = useAuth()
|
||||
const router = useRouter()
|
||||
const [error, setError] = React.useState<null | string>(null)
|
||||
|
||||
const {
|
||||
formState: { errors, isLoading },
|
||||
handleSubmit,
|
||||
register,
|
||||
} = useForm<FormData>({
|
||||
defaultValues: {
|
||||
email: 'demo@payloadcms.com',
|
||||
password: 'demo',
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
await login(data)
|
||||
if (redirect?.current) router.push(redirect.current)
|
||||
else router.push('/account')
|
||||
} catch (_) {
|
||||
setError('There was an error with the credentials provided. Please try again.')
|
||||
}
|
||||
},
|
||||
[login, router],
|
||||
)
|
||||
|
||||
return (
|
||||
<form className={classes.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<p>
|
||||
{'To log in, use the email '}
|
||||
<b>demo@payloadcms.com</b>
|
||||
{' with the password '}
|
||||
<b>demo</b>
|
||||
{'. To manage your users, '}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_SERVER_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<Message className={classes.message} error={error} />
|
||||
<Input
|
||||
error={errors.email}
|
||||
label="Email Address"
|
||||
name="email"
|
||||
register={register}
|
||||
required
|
||||
type="email"
|
||||
/>
|
||||
<Input
|
||||
error={errors.password}
|
||||
label="Password"
|
||||
name="password"
|
||||
register={register}
|
||||
required
|
||||
type="password"
|
||||
/>
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={classes.submit}
|
||||
disabled={isLoading}
|
||||
label={isLoading ? 'Processing' : 'Login'}
|
||||
type="submit"
|
||||
/>
|
||||
<div>
|
||||
<Link href={`/create-account${allParams}`}>Create an account</Link>
|
||||
<br />
|
||||
<Link href={`/recover-password${allParams}`}>Recover your password</Link>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@import "../_css/common";
|
||||
|
||||
.login {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
|
||||
.params {
|
||||
margin-top: var(--base);
|
||||
}
|
||||
28
examples/auth/payload/src/app/(app)/login/page.tsx
Normal file
28
examples/auth/payload/src/app/(app)/login/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { redirect } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import config from '../../../payload.config'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { RenderParams } from '../_components/RenderParams'
|
||||
import { LoginForm } from './LoginForm'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function Login() {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayloadHMR({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
if (user) {
|
||||
redirect(`/account?message=${encodeURIComponent('You are already logged in.')}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Gutter className={classes.login}>
|
||||
<RenderParams className={classes.params} />
|
||||
<h1>Log in</h1>
|
||||
<LoginForm />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
|
||||
import { useAuth } from '../../_providers/Auth'
|
||||
|
||||
export const LogoutPage: React.FC = () => {
|
||||
const { logout } = useAuth()
|
||||
const [success, setSuccess] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const performLogout = async () => {
|
||||
try {
|
||||
await logout()
|
||||
setSuccess('Logged out successfully.')
|
||||
} catch (_) {
|
||||
setError('You are already logged out.')
|
||||
}
|
||||
}
|
||||
|
||||
void performLogout()
|
||||
}, [logout])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{(error || success) && (
|
||||
<div>
|
||||
<h1>{error || success}</h1>
|
||||
<p>
|
||||
{'What would you like to do next? '}
|
||||
<Link href="/">Click here</Link>
|
||||
{` to go to the home page. To log back in, `}
|
||||
<Link href="/login">click here</Link>.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.logout {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
35
examples/auth/payload/src/app/(app)/logout/page.tsx
Normal file
35
examples/auth/payload/src/app/(app)/logout/page.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import config from '../../../payload.config'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { LogoutPage } from './LogoutPage'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function Logout() {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayloadHMR({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<Gutter className={classes.logout}>
|
||||
<h1>You are already logged out.</h1>
|
||||
<p>
|
||||
{'What would you like to do next? '}
|
||||
<Link href="/">Click here</Link>
|
||||
{` to go to the home page. To log back in, `}
|
||||
<Link href="/login">click here</Link>.
|
||||
</p>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Gutter className={classes.logout}>
|
||||
<LogoutPage />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
57
examples/auth/payload/src/app/(app)/page.tsx
Normal file
57
examples/auth/payload/src/app/(app)/page.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import Link from 'next/link'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import config from '../../payload.config'
|
||||
import { Gutter } from './_components/Gutter'
|
||||
import { HydrateClientUser } from './_components/HydrateClientUser'
|
||||
|
||||
export default async function HomePage() {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayloadHMR({ config })
|
||||
const { permissions, user } = await payload.auth({ headers })
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<HydrateClientUser permissions={permissions} user={user} />
|
||||
<Gutter>
|
||||
<h1>Payload Auth Example</h1>
|
||||
<p>
|
||||
{'This is a '}
|
||||
<Link href="https://payloadcms.com" rel="noopener noreferrer" target="_blank">
|
||||
Payload
|
||||
</Link>
|
||||
{' + '}
|
||||
<Link href="https://nextjs.org" rel="noopener noreferrer" target="_blank">
|
||||
Next.js
|
||||
</Link>
|
||||
{' app using the '}
|
||||
<Link href="https://nextjs.org/docs/app" rel="noopener noreferrer" target="_blank">
|
||||
App Router
|
||||
</Link>
|
||||
{' made explicitly for the '}
|
||||
<Link href="https://github.com/payloadcms/payload/tree/main/examples/auth">
|
||||
Payload Auth Example
|
||||
</Link>
|
||||
{". This example demonstrates how to implement Payload's "}
|
||||
<Link href="https://payloadcms.com/docs/authentication/overview">Authentication</Link>
|
||||
{
|
||||
' strategies using the Local API, as well as through HTTP via the REST and GraphQL APIs. To toggle between these two HTTP APIs, see `_layout.tsx`.'
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
{'Visit the '}
|
||||
<Link href="/login">login page</Link>
|
||||
{' to start the authentication flow. Once logged in, you will be redirected to the '}
|
||||
<Link href="/account">account page</Link>
|
||||
{` which is restricted to users only. To manage all users, `}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_SERVER_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</Gutter>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.formWrapper {
|
||||
width: 66.66%;
|
||||
|
||||
@include mid-break {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: var(--base);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: var(--base);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import React, { Fragment, useCallback, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { Button } from '../../_components/Button'
|
||||
import { Input } from '../../_components/Input'
|
||||
import { Message } from '../../_components/Message'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
}
|
||||
|
||||
export const RecoverPasswordForm: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
const {
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
register,
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(async (data: FormData) => {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/forgot-password`,
|
||||
{
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
},
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
setSuccess(true)
|
||||
setError('')
|
||||
} else {
|
||||
setError(
|
||||
'There was a problem while attempting to send you a password reset email. Please try again.',
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{!success && (
|
||||
<React.Fragment>
|
||||
<h1>Recover Password</h1>
|
||||
<div className={classes.formWrapper}>
|
||||
<p>
|
||||
{`Please enter your email below. You will receive an email message with instructions on
|
||||
how to reset your password. To manage all of your users, `}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_SERVER_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<form className={classes.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<Message className={classes.message} error={error} />
|
||||
<Input
|
||||
error={errors.email}
|
||||
label="Email Address"
|
||||
name="email"
|
||||
register={register}
|
||||
required
|
||||
type="email"
|
||||
/>
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={classes.submit}
|
||||
label="Recover Password"
|
||||
type="submit"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{success && (
|
||||
<React.Fragment>
|
||||
<h1>Request submitted</h1>
|
||||
<p>Check your email for a link that will allow you to securely reset your password.</p>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@import "../_css/common";
|
||||
|
||||
.recoverPassword {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { redirect } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import config from '../../../payload.config'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { RecoverPasswordForm } from './RecoverPasswordForm'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function RecoverPassword() {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayloadHMR({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
if (user) {
|
||||
redirect(`/account?message=${encodeURIComponent('Cannot recover password while logged in.')}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Gutter className={classes.recoverPassword}>
|
||||
<RecoverPasswordForm />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.form {
|
||||
width: 66.66%;
|
||||
|
||||
@include mid-break {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: var(--base);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { Button } from '../../_components/Button'
|
||||
import { Input } from '../../_components/Input'
|
||||
import { Message } from '../../_components/Message'
|
||||
import { useAuth } from '../../_providers/Auth'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type FormData = {
|
||||
password: string
|
||||
token: string
|
||||
}
|
||||
|
||||
export const ResetPasswordForm: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const { login } = useAuth()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const token = searchParams.get('token')
|
||||
|
||||
const {
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/reset-password`,
|
||||
{
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
},
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json()
|
||||
|
||||
// Automatically log the user in after they successfully reset password
|
||||
await login({ email: json.user.email, password: data.password })
|
||||
|
||||
// Redirect them to `/account` with success message in URL
|
||||
router.push('/account?success=Password reset successfully.')
|
||||
} else {
|
||||
setError('There was a problem while resetting your password. Please try again later.')
|
||||
}
|
||||
},
|
||||
[router, login],
|
||||
)
|
||||
|
||||
// when Next.js populates token within router,
|
||||
// reset form with new token value
|
||||
useEffect(() => {
|
||||
reset({ token: token || undefined })
|
||||
}, [reset, token])
|
||||
|
||||
return (
|
||||
<form className={classes.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<Message className={classes.message} error={error} />
|
||||
<Input
|
||||
error={errors.password}
|
||||
label="New Password"
|
||||
name="password"
|
||||
register={register}
|
||||
required
|
||||
type="password"
|
||||
/>
|
||||
<input type="hidden" {...register('token')} />
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={classes.submit}
|
||||
label="Reset Password"
|
||||
type="submit"
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.resetPassword {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
27
examples/auth/payload/src/app/(app)/reset-password/page.tsx
Normal file
27
examples/auth/payload/src/app/(app)/reset-password/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { redirect } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import config from '../../../payload.config'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { ResetPasswordForm } from './ResetPasswordForm'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function ResetPassword() {
|
||||
const headers = getHeaders()
|
||||
const payload = await getPayloadHMR({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
if (user) {
|
||||
redirect(`/account?message=${encodeURIComponent('Cannot reset password while logged in.')}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Gutter className={classes.resetPassword}>
|
||||
<h1>Reset Password</h1>
|
||||
<p>Please enter a new password below.</p>
|
||||
<ResetPasswordForm />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
segments: string[]
|
||||
}
|
||||
searchParams: {
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
|
||||
|
||||
export default NotFound
|
||||
@@ -0,0 +1,22 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
segments: string[]
|
||||
}
|
||||
searchParams: {
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,10 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
@@ -0,0 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
@@ -0,0 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
0
examples/auth/payload/src/app/(payload)/custom.scss
Normal file
0
examples/auth/payload/src/app/(payload)/custom.scss
Normal file
16
examples/auth/payload/src/app/(payload)/layout.tsx
Normal file
16
examples/auth/payload/src/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import configPromise from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import React from 'react'
|
||||
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
|
||||
|
||||
export default Layout
|
||||
@@ -1,62 +1,13 @@
|
||||
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],
|
||||
},
|
||||
auth: true,
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
// Email added by default
|
||||
// Add more fields as needed
|
||||
],
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ import type { AfterChangeHook } from 'payload/dist/collections/config/types'
|
||||
|
||||
export const loginAfterCreate: AfterChangeHook = async ({
|
||||
doc,
|
||||
req,
|
||||
req: { payload, body = {}, res },
|
||||
operation,
|
||||
req,
|
||||
req: { body = {}, payload, res },
|
||||
}) => {
|
||||
if (operation === 'create') {
|
||||
const { email, password } = body
|
||||
|
||||
if (email && password) {
|
||||
const { user, token } = await payload.login({
|
||||
const { token, user } = await payload.login({
|
||||
collection: 'users',
|
||||
data: { email, password },
|
||||
req,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { User } 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 }) => {
|
||||
export const protectRoles: FieldHook<User & { id: string }> = ({ data, req }) => {
|
||||
const isAdmin = req.user?.roles.includes('admin') || data.email === 'demo@payloadcms.com' // for the seed script
|
||||
|
||||
if (!isAdmin) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Payload } from 'payload'
|
||||
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
|
||||
|
||||
export const seed = async (payload: Payload): Promise<void> => {
|
||||
export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
@@ -8,61 +8,72 @@
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
users: User
|
||||
'payload-preferences': PayloadPreference
|
||||
'payload-migrations': PayloadMigration
|
||||
}
|
||||
globals: {}
|
||||
users: User;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
globals: {};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
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
|
||||
id: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
roles?: ('admin' | 'user')[] | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users'
|
||||
value: string | User
|
||||
}
|
||||
key?: string
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string
|
||||
name?: string
|
||||
batch?: number
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
export interface GeneratedTypes {
|
||||
collections: {
|
||||
users: User
|
||||
'payload-preferences': PayloadPreference
|
||||
'payload-migrations': PayloadMigration
|
||||
}
|
||||
}
|
||||
}
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
@@ -1,24 +1,21 @@
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack'
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
import { Users } from './collections/Users'
|
||||
import BeforeLogin from './components/BeforeLogin'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Users],
|
||||
admin: {
|
||||
bundler: webpackBundler(),
|
||||
components: {
|
||||
beforeLogin: [BeforeLogin],
|
||||
},
|
||||
},
|
||||
editor: slateEditor({}),
|
||||
db: mongooseAdapter({
|
||||
url: process.env.DATABASE_URI,
|
||||
}),
|
||||
collections: [Users],
|
||||
cors: [
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
|
||||
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
|
||||
@@ -27,7 +24,12 @@ export default buildConfig({
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
|
||||
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
|
||||
].filter(Boolean),
|
||||
db: mongooseAdapter({
|
||||
url: process.env.DATABASE_URI || '',
|
||||
}),
|
||||
editor: slateEditor({}),
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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,
|
||||
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()
|
||||
@@ -1,36 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"node_modules/*": [
|
||||
"./node_modules/*"
|
||||
]
|
||||
},
|
||||
"@/*": ["./src/*"],
|
||||
"@payload-config": ["src/payload.config.ts"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
],
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user