Compare commits

..

135 Commits

Author SHA1 Message Date
Elliot DeNolf
892b884745 chore(release): v3.0.0-beta.25 [skip ci] 2024-05-06 13:02:08 -04:00
Elliot DeNolf
e8dd0a7daf chore: more type updates (#6234) 2024-05-06 12:57:50 -04:00
Elliot DeNolf
26151e39c9 chore: package type export consistency (#6232) 2024-05-06 11:38:23 -04:00
Elliot DeNolf
453e331014 chore: package.json author consistency 2024-05-06 11:05:44 -04:00
Paul
7f72006020 chore(plugin-stripe)!: add types exports and rename types to be more consistent with other plugins (#6216)
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
2024-05-06 10:37:44 -04:00
Paul
3c13df3c2d chore(plugin-search): add types export and rename internal config (#6220)
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
2024-05-06 09:54:01 -04:00
Paul
d31af813e2 chore(plugin-nested-docs): add types export (#6219)
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
2024-05-06 09:52:24 -04:00
Paul
a85dc66a39 chore(plugin-form-builder): add types export (#6218)
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
2024-05-06 09:48:14 -04:00
Paul
9492f0ae29 chore(plugin-seo): export types (#6217) 2024-05-06 09:46:25 -04:00
Jarrod Flesch
51149c75ff fix: threads draft arg through for child resolvers in GraphQL queries (#6196) 2024-05-04 16:43:17 -04:00
Jarrod Flesch
56bedb821f chore: adjusts forgot pw email template (#6209) 2024-05-04 16:42:22 -04:00
Friggo
d3bca574aa feat(translations): add Slovak translation (#6114) 2024-05-04 17:26:29 -03:00
Alessio Gravili
c462bf229f feat(richtext-lexical)!: add FixedToolbarFeature (#6192)
BREAKING:

- The default inline toolbar has now been extracted into an `InlineToolbarFeature`. While it's part of the defaultFeatures, you might have to add it to your editor features if you are not including the defaultFeatures and still want to keep the inline toolbar (floating toolbar)
- Some types have been renamed, e.g. `InlineToolbarGroup` is now `ToolbarGroup`, and `InlineToolbarGroupItem` is now `ToolbarGroupItem`
- The `displayName` property of SlashMenuGroup and SlashMenuItem has been renamed to `label` to match the `label` prop of the toolbars
- The `inlineToolbarFeatureButtonsGroupWithItem`, `inlineToolbarFormatGroupWithItems` and `inlineToolbarTextDropdownGroupWithItems` exports have been renamed to `toolbarTextDropdownGroupWithItems`,  `toolbarFormatGroupWithItems`, `toolbarFeatureButtonsGroupWithItems`
2024-05-03 19:55:26 -04:00
Paul
8a452c42af fix(plugin-form-builder): custom formSubmission hooks overriding core ones (#6204) 2024-05-03 18:02:17 -03:00
Jacob Fletcher
07f2d74dc3 chore(examples/auth): migrates to 3.0 (#5877) 2024-05-03 16:28:27 -04:00
Elliot DeNolf
2ce65dc1e0 chore(release): v3.0.0-beta.24 [skip ci] 2024-05-03 14:56:47 -04:00
Elliot DeNolf
37be06448c ci: more resilient release script (#6202) 2024-05-03 14:39:26 -04:00
Elliot DeNolf
9c13089a2f chore: add dotenv to test dir [skip ci] 2024-05-03 13:42:01 -04:00
Paul
b642cb2d93 chore!: update plugin exports to be named and consistent (#6195)
BREAKING CHANGE: All plugins have been updated to use named exports and the names have been updated to be consistent.

// before
import { cloudStorage } from '@payloadcms/plugin-cloud-storage'
// current
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'

//before
import { payloadCloud } from '@payloadcms/plugin-cloud'
// current
import { payloadCloudPlugin } from '@payloadcms/plugin-cloud'

//before
import formBuilder from '@payloadcms/plugin-form-builder'
// current
import { formBuilderPlugin } from '@payloadcms/plugin-form-builder'

//before
import { nestedDocs } from '@payloadcms/plugin-nested-docs'
// current
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'

//before
import { redirects } from '@payloadcms/plugin-redirects'
// current
import { redirectsPlugin } from '@payloadcms/plugin-redirects'

// before
import search from '@payloadcms/plugin-search'
// current
import { searchPlugin } from '@payloadcms/plugin-search'

//before
import { sentry } from '@payloadcms/plugin-sentry'
// current
import { sentryPlugin } from '@payloadcms/plugin-sentry'

// before
import { seo } from '@payloadcms/plugin-seo'
// current
import { seoPlugin } from '@payloadcms/plugin-seo'
2024-05-03 13:36:36 -03:00
Elliot DeNolf
9e5d521567 ci: allow examples/* scopes 2024-05-03 11:41:16 -04:00
Jessica Chowdhury
6eabc99e01 chore: translate checkbox result in collection list view (#6165) 2024-05-03 10:44:43 -04:00
Jacob Fletcher
ea917dd811 feat(next): supports custom login redirects in initPage (#6186) 2024-05-03 09:48:57 -04:00
Jacob Fletcher
070d8e1731 feat(next): supports custom login redirects in initPage 2024-05-03 09:24:34 -04:00
Jacob Fletcher
664c60d2bc chore(next): restructures initPage 2024-05-03 09:17:17 -04:00
Jarrod Flesch
e25814e1ee fix: cascade graphql locales through relationships (#6166) 2024-05-03 08:33:53 -04:00
Jarrod Flesch
27ea117731 fix: only allow save after form is modified (#6189) 2024-05-03 08:28:37 -04:00
Alessio Gravili
7ab156e117 feat(richtext-lexical)!: finalize ClientFeature interface (#6191)
**BREAKING:**
If you have own, custom lexical features, there will be a bunch of breaking API changes for you. The saved JSON data is not affected.

- `floatingSelectToolbar` has been changed to `toolbarInline`

- `slashMenu.dynamicOptions `and `slashMenu.options` have been changed to `slashMenu.groups` and `slashMenu.dynamicGroups`

- `toolbarFixed.sections` is now `toolbarFixed.groups`

- Slash menu group `options` and toolbar group `entries` have both been renamed to `items`

- Toolbar group item `onClick` has been renamed to `onSelect` to match slash menu properties

- slashMenu item `onSelect` is no longer auto-wrapped inside an `editor.update`. If you perform editor updates in them, you have to wrap it inside an `editor.update` callback yourself. Within our own features this extra control has removed a good amount of unnecessary, nested `editor.update` calls, which is good

- Slash menu items are no longer initialized using the `new` keyword, as they are now types and no longer classes. You can convert them to an object and add the `key` property as an object property instead of an argument to the previous SlashMenuItem constructor

- CSS classnames for slash menu and toolbars, as well as their items, have changed

- `CheckListFeature` is now exported as and has been renamed to `ChecklistFeature`

For guidance on migration, check out how we migrated our own features in this PR's diff: https://github.com/payloadcms/payload/pull/6191/files
2024-05-02 21:38:15 -04:00
Paul
f2d415663f fix: ensures confirm password remains on form state (#6190) 2024-05-02 18:53:35 -03:00
Jarrod Flesch
bdf08a19d1 fix: moves ts-essentials to prod deps (#6187) 2024-05-02 16:37:05 -04:00
Elliot DeNolf
cb90e9f622 chore(release): v3.0.0-beta.23 [skip ci] 2024-05-02 16:05:19 -04:00
Elliot DeNolf
b05a5e1fb6 chore(deps): bump turborepo 2024-05-02 15:57:18 -04:00
Elliot DeNolf
92a5da1006 chore: move stripe postman out of src [skip ci] 2024-05-02 15:50:44 -04:00
Paul
75a95469b2 feat(plugin-stripe): update plugin stripe for v3 (#6019) 2024-05-02 16:19:27 -03:00
Jarrod Flesch
c0ae287d46 fix: reset password validations (#6153)
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
Co-authored-by: James <james@trbl.design>
Co-authored-by: Alessio Gravili <alessio@gravili.de>
2024-05-02 15:08:47 -04:00
Patrik
a2b92aa3ff fix(ui): watch "where" query param inside route and reset WhereBuilder (#6184) 2024-05-02 13:25:51 -04:00
Friggo
544a2285d3 chore(translations): czech translation improvements (#6078) 2024-05-02 12:54:18 -04:00
Elliot DeNolf
8c39950ea3 chore(release): v3.0.0-beta.22 [skip ci] 2024-05-02 12:27:28 -04:00
Yiannis Demetriades
6d642fe9b9 fix(templates): adds back missing CSS import in blank 3.0 template (#6183) 2024-05-02 11:30:58 -04:00
Jacob Fletcher
f175a741bc chore(next): exports initPage utility (#6182) 2024-05-02 11:22:13 -04:00
Jacob Fletcher
3290376f80 fix(next): ensures admin access only blocks admin routes 2024-05-02 11:07:43 -04:00
Jacob Fletcher
2b5c1ba99a chore(next): exports initPage utility 2024-05-02 10:32:17 -04:00
Alessio Gravili
bcb3f08386 chore: hide test flakes, improve playwright CI logs, significantly reduce playwright timeouts, add back test retries, cache playwright browsers in CI, disable CI telemetry, improve test throttle utility (#6155) 2024-05-01 17:35:41 -04:00
Wilson
b729b9bebd docs: add before login comments (#6101) 2024-05-01 15:59:11 -04:00
Tylan Davis
26ee91eb48 docs: adjust line breaks in code blocks (#6001) 2024-05-01 15:57:39 -04:00
Paul
43a17f67a0 chore(richtext-lexical): export types for additional props (#6173) 2024-05-01 15:03:06 -03:00
Elliot DeNolf
c71d2db949 chore(release): v3.0.0-beta.21 [skip ci] 2024-05-01 13:25:14 -04:00
Jacob Fletcher
04f1df8af1 fix(templates): updates payload app files (#6172) 2024-05-01 12:43:47 -04:00
Elliot DeNolf
1c490aee42 fix(deps): move file-type to deps (#6171) 2024-05-01 12:20:45 -04:00
Elliot DeNolf
c6132df866 chore: rename resend package (#6168) 2024-05-01 12:02:40 -04:00
Alessio Gravili
d8f91cc94c feat(richtext-lexical)!: various validation improvement (#6163)
BREAKING: this will now display errors if you're previously had invalid link or upload fields data - for example if you have a required field added to an uploads node and did not provide a value to it every time you've added an upload node
2024-05-01 11:33:02 -04:00
Alessio Gravili
568b074809 fix: various loader issues (#6090) 2024-05-01 10:45:28 -04:00
Alessio Gravili
401c16e485 chore: lexical int tests: do not use relationTo to collection with rich text relationships disabled 2024-05-01 00:47:40 -04:00
Elliot DeNolf
17bee6a145 ci: reworks changelog and release notes generation (#6164) 2024-04-30 23:50:49 -04:00
Alessio Gravili
8829fba6cf feat(richtext-lexical)!: add validation to link and upload nodes
BREAKING: this will now display errors if you're previously had invalid link or upload fields data - for example if you have a required field added to an uploads node and did not provide a value to it every time you've added an upload node
2024-04-30 23:14:27 -04:00
Alessio Gravili
e8983abe65 ci: enable fields RichText e2e test suite 2024-04-30 23:12:47 -04:00
Alessio Gravili
01f38c4e33 feat(richtext-lexical): link node: disable client-side link validation. This allows overriding validation behavior by providing your own url field to the Link feature.
Allows you to, for example, allow anchor nodes to be inputted as URL by overriding validation.
2024-04-30 23:12:07 -04:00
Alessio Gravili
10b99ceb6f fix: add missing error logger to buildFormState error catch 2024-04-30 23:10:52 -04:00
Alessio Gravili
1140426b73 Merge remote-tracking branch 'origin/beta' into feat/improve-lexical-validations-2 2024-04-30 23:02:07 -04:00
Alessio Gravili
5a82f34801 feat(richtext-lexical)!: change link fields handling (#6162)
**BREAKING:**
- Drawer fields are no longer wrapped in a `fields` group. This might be breaking if you depend on them being in a field group in any way - potentially if you use custom link fields. This does not change how the data is saved
- If you pass in an array of custom fields to the link feature, those were previously added to the base fields. Now, they completely replace the base fields for consistency. If you want to ADD fields to the base fields now, you will have to pass in a function and spread `defaultFields` - similar to how adding your own features to lexical works

**Example Migration for ADDING fields to the link base fields:**

**Previous:**
```ts
 LinkFeature({
    fields: [
      {
        name: 'rel',
        label: 'Rel Attribute',
        type: 'select',
        hasMany: true,
        options: ['noopener', 'noreferrer', 'nofollow'],
        admin: {
          description:
            'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
        },
      },
    ],
  }),
```

**Now:**
```ts
 LinkFeature({
    fields: ({ defaultFields }) => [
      ...defaultFields,
      {
        name: 'rel',
        label: 'Rel Attribute',
        type: 'select',
        hasMany: true,
        options: ['noopener', 'noreferrer', 'nofollow'],
        admin: {
          description:
            'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
        },
      },
    ],
  }),
2024-04-30 23:01:08 -04:00
Alessio Gravili
5420d889fe fix(richtext-slate): do not add empty fields group if no custom fields are added 2024-04-30 21:53:47 -04:00
Alessio Gravili
d9bb51fdc7 feat(richtext-lexical)!: initialize lexical during sanitization (#6119)
BREAKING:

- sanitizeFields is now an async function
- the richText adapters now return a function instead of returning the adapter directly
2024-04-30 15:09:32 -04:00
Alessio Gravili
9a636a3cfb fix(richtext-lexical): floating toolbar caret positioned incorrectly for some line heights (#6149) 2024-04-30 12:01:25 -04:00
Alessio Gravili
181f82f33e feat(richtext-lexical): implement relationship node click and delete/backspace handling (#6147) 2024-04-30 11:11:47 -04:00
Alessio Gravili
6a9cde24b0 fix(richtext-lexical): drag and add block handles disappear too quickly for smaller screen sizes. (#6144) 2024-04-30 10:50:18 -04:00
Elliot DeNolf
dc31d9c715 test: parse and update tsconfig in before test hook 2024-04-30 00:24:06 -04:00
Elliot DeNolf
45b3f06e1b chore: implement better tsconfig reset mechanism 2024-04-29 23:23:09 -04:00
Elliot DeNolf
d5f7944ac4 chore(eslint): set prefer-ts-expect-error to error 2024-04-29 22:30:05 -04:00
Elliot DeNolf
3d50caf985 feat: implement resend rest email adapter (#5916) 2024-04-29 22:06:53 -04:00
Jacob Fletcher
4d7ef58e7e fix: blocks non-admin users from admin access (#6127) 2024-04-29 19:53:18 -04:00
Paul
3e117f4e99 chore: add graphql as a dependency to the blank template (#6128) 2024-04-29 20:09:27 -03:00
Elliot DeNolf
888d6f8856 ci(scripts): adjust release publish limit 2024-04-29 17:19:36 -04:00
Elliot DeNolf
9ebf8693d4 chore(release): v3.0.0-beta.20 [skip ci] 2024-04-29 16:51:35 -04:00
James Mikrut
d8c3127b09 fix: local req missing url headers (#6126)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2024-04-29 16:40:59 -04:00
James Mikrut
b6631f4778 fix: logout-inactivity route was 404ing (#6121) 2024-04-29 15:53:08 -04:00
Elliot DeNolf
2e77bdf11e test: add test email adapter, use for all tests by default (#6120) 2024-04-29 14:38:35 -04:00
Elliot DeNolf
cce75f11ca ci: add better message for PR subject pattern error 2024-04-29 14:34:04 -04:00
Elliot DeNolf
d8b6b39dbb fix: validate user slug is an auth-enabled collection (#6118) 2024-04-29 14:25:23 -04:00
Jacob Fletcher
fa89057aac fix(next,ui): properly sets document operation for globals (#6116) 2024-04-29 13:58:06 -04:00
Jarrod Flesch
15db0a8018 fix: conditions throwing errors break form state (#6113) 2024-04-29 12:54:00 -04:00
Elliot DeNolf
b7a4d9cea4 chore(deps): adjust node engines to account for node register requirement (#6115) 2024-04-29 12:28:31 -04:00
Elliot DeNolf
5b676c36e5 docs(storage-*): readme fixes 2024-04-29 09:40:39 -04:00
Jacob Fletcher
32231762ff chore: version permissions (#6068) 2024-04-29 08:22:56 -04:00
Elliot DeNolf
a7096c1599 chore: telemetry localization (#6075) 2024-04-28 22:17:08 -04:00
Alessio Gravili
bed428c27e feat(richtext-lexical)!: upgrade lexical from 0.13.1 to 0.14.5 and backport other changes (#6095)
BREAKING:

- Lexical may introduce breaking changes in their updates. Please consult their changelog. One breaking change I noticed is that the SerializedParagraphNode now has a new, required textFormat property.

- Now that lexical supports ESM, all CJS-style imports have been changed to ESM-style imports. You may have to do the same in your codebase if you import from lexical core packages
2024-04-28 20:25:27 -04:00
Elliot DeNolf
873e698352 docs: storage-* and plugin-cloud-storage updates (#6096) 2024-04-28 20:07:49 -04:00
Alessio Gravili
ad13577399 chore(richtext-lexical): fix build and backport badNode logic change from lexical core 2024-04-28 19:58:07 -04:00
Alessio Gravili
31a9c77055 fix(richtext-lexical): preserve bullet list item indent on newline
BACKPORTS https://github.com/facebook/lexical/pull/5578
2024-04-28 19:30:52 -04:00
Alessio Gravili
bae0c2df5f fix(richtext-lexical): add missing uuid dependency 2024-04-28 19:26:25 -04:00
Alessio Gravili
0ed31def68 feat(richtext-lexical): implement upload node click and delete/backspace handling 2024-04-28 19:19:34 -04:00
Alessio Gravili
0e7a6ad5ab fix(richtext-lexical): prevent link modal from showing if selection spans further than the link
Backports https://github.com/facebook/lexical/pull/5551
2024-04-28 19:03:02 -04:00
Alessio Gravili
180797540c feat(richtext-lexical)!: change all CJS lexical imports to ESM lexical imports
BREAKING: You might have to do the same if you import from lexical in your application
2024-04-28 18:50:58 -04:00
Alessio Gravili
c00babf9b3 chore(richtext-lexical): upgrade all lexical dependencies from 0.13.1 to 0.14.5 2024-04-28 17:52:11 -04:00
Alessio Gravili
943681ae3c chore: upgrade typescript from 5.4.4 to 5.4.5 (#6093) 2024-04-28 17:46:41 -04:00
Alessio Gravili
f14ce367d2 fix(richtext-lexical): type errors for FeatureProviderServer with typescript strict mode (#6091) 2024-04-28 17:12:47 -04:00
Paul
3eb5766323 fix: issue with dupplicate ':' in email links (#6086) 2024-04-28 17:22:07 -03:00
Alessio Gravili
cd5e8d7b52 fix: importWithoutClientFiles not working due to incorrect import path used 2024-04-28 16:06:01 -04:00
Alessio Gravili
361d12e97c chore: loader test: use importConfig helper instead of manually registering loader to realistically test what a user would experience 2024-04-28 16:04:11 -04:00
Elliot DeNolf
fb4a5a3715 chore(ui): fix bad imports 2024-04-28 14:53:15 -04:00
Elliot DeNolf
9c2585ba86 chore(eslint): no-relative-monorepo-imports on package dir, other cleanup 2024-04-28 14:49:48 -04:00
Paul
feb6296bb4 chore: add tailwind and shadcn/ui example (#6085) 2024-04-28 15:06:43 -03:00
Alessio Gravili
74eb71c304 chore: add failing loader test case 2024-04-27 21:13:38 -04:00
Alessio Gravili
fa2083f764 fix: loader: typescript module resolver not resolving to source path of symlinked module 2024-04-27 20:45:04 -04:00
Jacob Fletcher
7111834a99 fix(ui): conditionally fetches versions based on read access 2024-04-26 17:40:28 -04:00
Jacob Fletcher
a943c7eddb fix(ui): conditionally renders versions tab based on read access 2024-04-26 17:40:14 -04:00
Jacob Fletcher
2d089a7bae chore: threads permissions through document tab conditions 2024-04-26 17:38:59 -04:00
Elliot DeNolf
5bba969f0d chore(release): v3.0.0-beta.19 [skip ci] 2024-04-26 17:08:50 -04:00
Jarrod Flesch
3a43fd34c0 chore: fixes bad auto import (#6070) 2024-04-26 16:45:24 -04:00
Jarrod Flesch
d9005b3f53 chore: file uploads, broken import path (#6069) 2024-04-26 16:26:11 -04:00
Jarrod Flesch
fab9e32175 fix: correct createPayloadRequest routeParams (#6059) 2024-04-26 16:13:21 -04:00
Jarrod Flesch
e71c1c2ec4 fix: formData handling on Vercel (#6067) 2024-04-26 16:10:14 -04:00
Dan Ribbens
81fb0515fb fix: bulk publish from collection list (#6065) 2024-04-26 15:46:02 -04:00
Elliot DeNolf
739dfc1434 chore: convert all errors to named exports (#6061) 2024-04-26 13:24:18 -04:00
Jacob Fletcher
a4e8795666 fix(deps): dedupes react (#6064) 2024-04-26 13:22:12 -04:00
Elliot DeNolf
14134d637d fix: properly handle external file url (#6060) 2024-04-26 12:02:07 -04:00
Elliot DeNolf
2b698a9018 chore: add more valid pr scopes [skip ci] 2024-04-26 11:55:01 -04:00
Elliot DeNolf
91684c8a7d ci: app build with packed (#6051) 2024-04-26 00:19:44 -04:00
Elliot DeNolf
fbdfe1d9dd chore: set -ex on pack and build step 2024-04-25 23:56:16 -04:00
Elliot DeNolf
7221725121 chore: start mongo for build 2024-04-25 23:46:48 -04:00
Elliot DeNolf
640348df3a chore: use --ignore-workspace in template install 2024-04-25 23:37:05 -04:00
Elliot DeNolf
4ed99e017a ci: add app-build-with-packed job 2024-04-25 23:28:28 -04:00
Elliot DeNolf
df6b9dd30b ci(scripts): update pack-all-to-dest 2024-04-25 23:25:31 -04:00
Elliot DeNolf
faf142baff chore: sort package.json files (#6050) 2024-04-25 22:41:55 -04:00
Elliot DeNolf
f80cb9f553 chore: clean up package.json descriptions and keywords 2024-04-25 22:39:03 -04:00
Elliot DeNolf
d3eaa1fceb chore: add sort-package-json to lint-staged 2024-04-25 22:23:58 -04:00
Elliot DeNolf
df77152851 chore: add sort-package-json, sort all package.json files 2024-04-25 22:19:37 -04:00
Elliot DeNolf
937202b27c fix(deps): remove monorepo deps 2024-04-25 22:12:44 -04:00
Paul
3581f39c31 chore: update whitelabel example (#6049) 2024-04-25 17:24:42 -03:00
Paul
c1d9c81b68 chore: update virtual fields example (#6043) 2024-04-25 16:13:10 -03:00
Jarrod Flesch
20355a4dd4 fix: version restoration (#6040) 2024-04-25 14:15:12 -04:00
Elliot DeNolf
cf66d7f09b docs: new packages (#6041) 2024-04-25 13:14:52 -04:00
Elliot DeNolf
30afe81462 docs: add docs for all new storage packages 2024-04-25 13:08:32 -04:00
Elliot DeNolf
18ee6e8867 docs: add docs for email-nodemailer 2024-04-25 13:08:18 -04:00
Paul
9f78a93403 chore: update hierarchy example (#6036) 2024-04-25 12:54:17 -03:00
Dan Ribbens
bd046e2437 fix(db-postgres): use locales suffix (#6032) 2024-04-25 11:07:52 -04:00
Elliot DeNolf
e9004a93a4 ci(scripts): safer package details retrieval 2024-04-25 10:50:41 -04:00
752 changed files with 48520 additions and 34440 deletions

View File

@@ -8,6 +8,7 @@ module.exports = {
plugins: ['payload'],
rules: {
'payload/no-jsx-import-statements': 'warn',
'payload/no-relative-monorepo-imports': 'error',
},
},
{

View File

@@ -13,6 +13,8 @@ concurrency:
env:
NODE_VERSION: 18.20.2
PNPM_VERSION: 8.15.7
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
jobs:
changes:
@@ -89,6 +91,8 @@ jobs:
- run: pnpm install
- run: pnpm run build:all
env:
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
- name: Cache build
uses: actions/cache@v4
@@ -243,6 +247,7 @@ jobs:
- fields__collections__Blocks
- fields__collections__Array
- fields__collections__Relationship
- fields__collections__RichText
- fields__collections__Lexical
- live-preview
- localization
@@ -252,7 +257,8 @@ jobs:
- plugin-seo
- versions
- uploads
env:
SUITE_NAME: ${{ matrix.suite }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
@@ -280,11 +286,33 @@ jobs:
run: pnpm docker:start
if: ${{ matrix.suite == 'plugin-cloud-storage' }}
- name: Install Playwright
run: pnpm exec playwright install --with-deps
- name: Store Playwright's Version
run: |
# Extract the version number using a more targeted regex pattern with awk
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --depth=0 | awk '/@playwright\/test/ {print $2}')
echo "Playwright's Version: $PLAYWRIGHT_VERSION"
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
- name: Cache Playwright Browsers for Playwright's Version
id: cache-playwright-browsers
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
- name: Setup Playwright - Browsers and Dependencies
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium
- name: Setup Playwright - Dependencies-only
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromium
- name: E2E Tests
run: pnpm test:e2e ${{ matrix.suite }}
run: PLAYWRIGHT_JSON_OUTPUT_NAME=results_${{ matrix.suite }}.json pnpm test:e2e ${{ matrix.suite }}
env:
PLAYWRIGHT_JSON_OUTPUT_NAME: results_${{ matrix.suite }}.json
NEXT_TELEMETRY_DISABLED: 1
- uses: actions/upload-artifact@v4
if: always()
@@ -294,6 +322,57 @@ jobs:
if-no-files-found: ignore
retention-days: 1
# Disabled until this is fixed: https://github.com/daun/playwright-report-summary/issues/156
# - uses: daun/playwright-report-summary@v3
# with:
# report-file: results_${{ matrix.suite }}.json
# report-tag: ${{ matrix.suite }}
# job-summary: true
app-build-with-packed:
runs-on: ubuntu-latest
needs: build
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.10.0
with:
mongodb-version: 6.0
- name: Pack and build app
run: |
set -ex
pnpm run script:pack --dest templates/blank-3.0
cd templates/blank-3.0
cp .env.example .env
ls -la
pnpm add ./*.tgz
pnpm install --ignore-workspace
cat package.json
pnpm run build
tests-type-generation:
if: false # This should be replaced with gen on a real Payload project
runs-on: ubuntu-latest

View File

@@ -50,6 +50,7 @@ jobs:
plugin-form-builder
plugin-nested-docs
plugin-redirects
plugin-relationship-object-ids
plugin-search
plugin-sentry
plugin-seo
@@ -65,10 +66,15 @@ jobs:
translations
ui
templates
examples
examples(\/(\w|-)+)?
deps
# Disallow uppercase letters at the beginning of the subject
subjectPattern: ^(?![A-Z]).+$
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}"
didn't match the configured pattern. Please ensure that the subject
doesn't start with an uppercase character.
- uses: marocchino/sticky-pull-request-comment@v2
# When the previous steps fails, the workflow would stop. By adding this

7
.vscode/launch.json vendored
View File

@@ -58,6 +58,13 @@
"PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER": "s3"
}
},
{
"command": "node --no-deprecation test/dev.js collections-graphql",
"cwd": "${workspaceFolder}",
"name": "Run Dev GraphQL",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js fields",
"cwd": "${workspaceFolder}",

View File

@@ -40,5 +40,6 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
},
"files.insertFinalNewline": true
}

View File

@@ -1,39 +0,0 @@
module.exports = {
// gitRawCommitsOpts: {
// from: 'v2.0.9',
// path: 'packages/payload',
// },
// infile: 'CHANGELOG.md',
options: {
preset: {
name: 'conventionalcommits',
types: [
{ section: 'Features', type: 'feat' },
{ section: 'Features', type: 'feature' },
{ section: 'Bug Fixes', type: 'fix' },
{ section: 'Documentation', type: 'docs' },
],
},
},
// outfile: 'NEW.md',
writerOpts: {
commitGroupsSort: (a, b) => {
const groupOrder = ['Features', 'Bug Fixes', 'Documentation']
return groupOrder.indexOf(a.title) - groupOrder.indexOf(b.title)
},
// Scoped commits at the end, alphabetical sort
commitsSort: (a, b) => {
if (a.scope || b.scope) {
if (!a.scope) return -1
if (!b.scope) return 1
return a.scope === b.scope
? a.subject.localeCompare(b.subject)
: a.scope.localeCompare(b.scope)
}
// Alphabetical sort
return a.subject.localeCompare(b.subject)
},
},
}

View File

@@ -49,7 +49,8 @@ export default buildConfig({
{
label: 'Arabic',
code: 'ar',
// opt-in to setting default text-alignment on Input fields to rtl (right-to-left) when current locale is rtl
// opt-in to setting default text-alignment on Input fields to rtl (right-to-left)
// when current locale is rtl
rtl: true,
},
],
@@ -134,13 +135,9 @@ to support localization, you need to specify each field that you would like to l
```js
{
name: 'title',
type
:
'text',
// highlight-start
localized
:
true,
type: 'text',
// highlight-start
localized: true,
// highlight-end
}
```

View File

@@ -20,7 +20,8 @@ The initial request made to Payload will begin a new transaction and attach it t
```ts
const afterChange: CollectionAfterChangeHook = async ({ req }) => {
// because req.transactionID is assigned from Payload and passed through, my-slug will only persist if the entire request is successful
// because req.transactionID is assigned from Payload and passed through,
// my-slug will only persist if the entire request is successful
await req.payload.create({
req,
collection: 'my-slug',

View File

@@ -196,7 +196,8 @@ const Pages: CollectionConfig = {
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
// The HTMLConverter Feature is the feature which manages the HTML serializers. If you do not pass any arguments to it, it will use the default serializers.
// The HTMLConverter Feature is the feature which manages the HTML serializers.
// If you do not pass any arguments to it, it will use the default serializers.
HTMLConverterFeature({}),
],
}),

View File

@@ -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.

View File

@@ -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>

View File

@@ -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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,12 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp
playwright.config.ts
jest.config.js

View File

@@ -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'],
}

View File

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

View File

@@ -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
View 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.

View 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)

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}

View File

@@ -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>
)
}

View File

@@ -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);
}

View File

@@ -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'

View File

@@ -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;
}

View File

@@ -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>
)
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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>
)
}

View File

@@ -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);
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1,9 @@
.richText {
:first-child {
margin-top: 0;
}
a {
text-decoration: underline;
}
}

View File

@@ -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

View File

@@ -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

View 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;
}

View 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);
}

View File

@@ -0,0 +1 @@
@forward './queries.scss';

View 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;
}
}

View 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);
}
}
}
}

View 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;
}
}

View 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)
}
}

View 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)

View 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)
}
}

View 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
}

View File

@@ -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);
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,7 @@
.account {
margin-bottom: var(--block-padding);
}
.params {
margin-top: var(--base);
}

View 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>
)
}

View File

@@ -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);
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,5 @@
@import "../_css/common";
.createAccount {
margin-bottom: var(--block-padding);
}

View 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>
)
}

View 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>
)
}

View File

@@ -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);
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,9 @@
@import "../_css/common";
.login {
margin-bottom: var(--block-padding);
}
.params {
margin-top: var(--base);
}

View 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>
)
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,3 @@
.logout {
margin-bottom: var(--block-padding);
}

View 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>
)
}

View 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>
)
}

View File

@@ -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);
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,5 @@
@import "../_css/common";
.recoverPassword {
margin-bottom: var(--block-padding);
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,13 @@
@import "../../_css/common";
.form {
width: 66.66%;
@include mid-break {
width: 100%;
}
}
.submit {
margin-top: var(--base);
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,3 @@
.resetPassword {
margin-bottom: var(--block-padding);
}

View 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>
)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View 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

View File

@@ -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
],
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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: {

View File

@@ -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 {}
}

View File

@@ -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'),
},
})

View File

@@ -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()

View File

@@ -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

View File

@@ -1,2 +1,2 @@
DATABASE_URI=mongodb://127.0.0.1/payload-template-blank
DATABASE_URI=mongodb://127.0.0.1/payload-template-blank-3-0
PAYLOAD_SECRET=YOUR_SECRET_HERE

View File

@@ -0,0 +1,7 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
}

View File

@@ -1,6 +1,43 @@
build
dist
/media
node_modules
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
/.idea/*
!/.idea/runConfigurations
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env
/media

View File

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

Some files were not shown because too many files have changed in this diff Show More