feat: auth example
This commit is contained in:
7
examples/auth/cms/.env.example
Normal file
7
examples/auth/cms/.env.example
Normal file
@@ -0,0 +1,7 @@
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3000
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:8000
|
||||
MONGODB_URI=mongodb://localhost/payload-example-auth
|
||||
PAYLOAD_SECRET=PAYLOAD_AUTH_EXAMPLE_SECRET_KEY
|
||||
COOKIE_DOMAIN=localhost
|
||||
|
||||
|
||||
4
examples/auth/cms/.eslintrc.js
Normal file
4
examples/auth/cms/.eslintrc.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['./eslint-config'],
|
||||
}
|
||||
5
examples/auth/cms/.gitignore
vendored
Normal file
5
examples/auth/cms/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
build
|
||||
dist
|
||||
node_modules
|
||||
package-lock.json
|
||||
.env
|
||||
1
examples/auth/cms/.npmrc
Normal file
1
examples/auth/cms/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
legacy-peer-deps=true
|
||||
8
examples/auth/cms/.prettierrc.js
Normal file
8
examples/auth/cms/.prettierrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: "typescript",
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
arrowParens: "avoid",
|
||||
};
|
||||
42
examples/auth/cms/README.md
Normal file
42
examples/auth/cms/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Auth Example for Payload CMS
|
||||
|
||||
This example demonstrates how to implement [Authentication](https://payloadcms.com/docs/authentication/overview) in Payload CMS.
|
||||
|
||||
There is a fully working Next.js app tailored specifically for this example which can be found [here](../nextjs). Follow the instructions there to get started. If you are setting up authentication for another front-end, please consider contributing to this repo with your own example!
|
||||
|
||||
## Getting Started
|
||||
|
||||
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:8000/admin` to access the admin panel
|
||||
6. Login with email `dev@payloadcms.com` and password `test`
|
||||
|
||||
## How it works
|
||||
|
||||
An auth-enabled `users` collection is create which opens all [auth-related operations](https://payloadcms.com/docs/authentication/operations) needed to create a fully custom workflow on your front-end using the REST or GraphQL APIs, including:
|
||||
|
||||
- `Me`
|
||||
- `Login`
|
||||
- `Logout`
|
||||
- `Refresh Token`
|
||||
- `Verify Email`
|
||||
- `Unlock`
|
||||
- `Forgot Password`
|
||||
- `Reset Password`
|
||||
|
||||
The [`cors`](https://payloadcms.com/docs/production/preventing-abuse#cross-origin-resource-sharing-cors), [`csrf`](https://payloadcms.com/docs/production/preventing-abuse#cross-site-request-forgery-csrf), and [`cookies`](https://payloadcms.com/docs/authentication/config#options) settings are also configured to ensure that the admin panel and front-end can communicate with each other securely.
|
||||
|
||||
### Role-based Access Control
|
||||
|
||||
Basic role-based access control is setup to determine what users can and cannot do based on their roles, which are:
|
||||
|
||||
- `admin`: They can access the Payload admin panel to manage your application. They can see all data and make all operations.
|
||||
- `user`: They cannot access the Payload admin panel and have a limited access to operations based on their user.
|
||||
|
||||
A `beforeChange` field hook called `protectRoles` is placed on this to automatically populate `roles` with the `user` role when a new user is created. It also protects roles from being changed by non-admins.
|
||||
|
||||
### Seed
|
||||
|
||||
On boot, a seed script is included to create a user with the role `admin`.
|
||||
15
examples/auth/cms/eslint-config/index.js
Normal file
15
examples/auth/cms/eslint-config/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
require.resolve('./rules/style.js'),
|
||||
require.resolve('./rules/import.js'),
|
||||
require.resolve('./rules/typescript.js'),
|
||||
require.resolve('./rules/prettier.js'),
|
||||
],
|
||||
env: {
|
||||
es6: true,
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
}
|
||||
32
examples/auth/cms/eslint-config/rules/import.js
Normal file
32
examples/auth/cms/eslint-config/rules/import.js
Normal file
@@ -0,0 +1,32 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
},
|
||||
extends: ['plugin:import/errors', 'plugin:import/warnings', 'plugin:import/typescript'],
|
||||
plugins: ['import'],
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'import/no-unresolved': ['error', { commonjs: true, caseSensitive: true }],
|
||||
'import/no-default-export': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
ts: 'never',
|
||||
tsx: 'never',
|
||||
js: 'never',
|
||||
jsx: 'never',
|
||||
},
|
||||
],
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/named': 'error',
|
||||
'import/no-relative-packages': 'warn',
|
||||
'import/no-import-module-exports': 'warn',
|
||||
'import/no-cycle': 'warn',
|
||||
},
|
||||
}
|
||||
7
examples/auth/cms/eslint-config/rules/prettier.js
Normal file
7
examples/auth/cms/eslint-config/rules/prettier.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: ['prettier'],
|
||||
extends: ['plugin:prettier/recommended'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
}
|
||||
13
examples/auth/cms/eslint-config/rules/style.js
Normal file
13
examples/auth/cms/eslint-config/rules/style.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'prefer-named-exports': 'off',
|
||||
'prefer-destructuring': 'off',
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
'class-methods-use-this': 'off',
|
||||
'function-paren-newline': ['error', 'consistent'],
|
||||
'eol-last': ['error', 'always'],
|
||||
'no-restricted-syntax': 'off',
|
||||
'no-await-in-loop': 'off',
|
||||
'no-console': 'error',
|
||||
},
|
||||
}
|
||||
322
examples/auth/cms/eslint-config/rules/typescript.js
Normal file
322
examples/auth/cms/eslint-config/rules/typescript.js
Normal file
@@ -0,0 +1,322 @@
|
||||
module.exports = {
|
||||
plugins: ['@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/**.ts', '**/**.d.ts'],
|
||||
rules: {
|
||||
'no-undef': 'off',
|
||||
camelcase: 'off',
|
||||
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
||||
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
|
||||
'@typescript-eslint/await-thenable': 'off',
|
||||
'@typescript-eslint/consistent-type-assertions': [
|
||||
'error',
|
||||
{ assertionStyle: 'as', objectLiteralTypeAssertions: 'allow-as-parameter' },
|
||||
],
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||||
'@typescript-eslint/consistent-type-imports': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'error',
|
||||
{
|
||||
allowExpressions: true,
|
||||
allowTypedFunctionExpressions: true,
|
||||
allowHigherOrderFunctions: true,
|
||||
allowConciseArrowFunctionExpressionsStartingWithVoid: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-member-accessibility': [
|
||||
'error',
|
||||
{ accessibility: 'no-public' },
|
||||
],
|
||||
'@typescript-eslint/member-delimiter-style': [
|
||||
'error',
|
||||
{
|
||||
multiline: {
|
||||
delimiter: 'none',
|
||||
requireLast: true,
|
||||
},
|
||||
singleline: {
|
||||
delimiter: 'semi',
|
||||
requireLast: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/method-signature-style': 'off',
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'off',
|
||||
{
|
||||
selector: 'default',
|
||||
format: ['camelCase'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: ['camelCase', 'UPPER_CASE'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: 'typeParameter',
|
||||
format: ['PascalCase'],
|
||||
prefix: ['T', 'U'],
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
types: ['boolean'],
|
||||
format: ['PascalCase'],
|
||||
prefix: ['is', 'should', 'has', 'can', 'did', 'will'],
|
||||
},
|
||||
{
|
||||
selector: 'interface',
|
||||
format: ['PascalCase'],
|
||||
custom: {
|
||||
regex: '^I[A-Z]',
|
||||
match: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: [
|
||||
'function',
|
||||
'parameter',
|
||||
'property',
|
||||
'parameterProperty',
|
||||
'method',
|
||||
'accessor',
|
||||
],
|
||||
format: ['camelCase'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: ['class', 'interface', 'typeAlias', 'enum', 'typeParameter'],
|
||||
format: ['PascalCase'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-base-to-string': 'off',
|
||||
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
|
||||
'@typescript-eslint/no-dynamic-delete': 'error',
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
'@typescript-eslint/no-explicit-any': [
|
||||
'warn',
|
||||
{
|
||||
ignoreRestArgs: true,
|
||||
// enable later
|
||||
fixToUnknown: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-extra-non-null-assertion': 'error',
|
||||
'@typescript-eslint/no-extraneous-class': [
|
||||
'error',
|
||||
{
|
||||
allowConstructorOnly: false,
|
||||
allowEmpty: false,
|
||||
allowStaticOnly: false,
|
||||
allowWithDecorator: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-for-in-array': 'off',
|
||||
'@typescript-eslint/no-implicit-any-catch': [
|
||||
'error',
|
||||
{
|
||||
allowExplicitAny: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-implied-eval': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': [
|
||||
'error',
|
||||
{
|
||||
ignoreParameters: false,
|
||||
ignoreProperties: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-invalid-void-type': [
|
||||
'off',
|
||||
{
|
||||
allowInGenericTypeArguments: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-misused-new': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'@typescript-eslint/no-parameter-properties': 'error',
|
||||
'@typescript-eslint/no-require-imports': 'error',
|
||||
'@typescript-eslint/no-this-alias': 'error',
|
||||
'@typescript-eslint/no-throw-literal': 'off',
|
||||
'@typescript-eslint/no-type-alias': [
|
||||
'off',
|
||||
{
|
||||
allowAliases: 'always',
|
||||
allowCallbacks: 'always',
|
||||
allowConditionalTypes: 'always',
|
||||
allowConstructors: 'never',
|
||||
allowLiterals: 'in-unions-and-intersections',
|
||||
allowMappedTypes: 'always',
|
||||
allowTupleTypes: 'always',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
'@typescript-eslint/no-unnecessary-qualifier': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-arguments': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'error',
|
||||
'@typescript-eslint/prefer-as-const': 'error',
|
||||
'@typescript-eslint/prefer-enum-initializers': 'off',
|
||||
'@typescript-eslint/prefer-for-of': 'error',
|
||||
'@typescript-eslint/prefer-includes': 'off',
|
||||
'@typescript-eslint/prefer-literal-enum-member': 'error',
|
||||
'@typescript-eslint/prefer-namespace-keyword': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/prefer-readonly': 'off',
|
||||
'@typescript-eslint/prefer-readonly-parameter-types': 'off',
|
||||
'@typescript-eslint/prefer-reduce-type-parameter': 'off',
|
||||
'@typescript-eslint/prefer-regexp-exec': 'off',
|
||||
'@typescript-eslint/prefer-string-starts-ends-with': 'off',
|
||||
'@typescript-eslint/prefer-ts-expect-error': 'warn',
|
||||
'@typescript-eslint/promise-function-async': 'off',
|
||||
'@typescript-eslint/require-array-sort-compare': 'off',
|
||||
'@typescript-eslint/restrict-plus-operands': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||
'@typescript-eslint/switch-exhaustiveness-check': 'off',
|
||||
'@typescript-eslint/triple-slash-reference': 'error',
|
||||
'@typescript-eslint/type-annotation-spacing': [
|
||||
'error',
|
||||
{
|
||||
before: false,
|
||||
after: true,
|
||||
overrides: {
|
||||
arrow: {
|
||||
before: true,
|
||||
after: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/typedef': [
|
||||
'error',
|
||||
{
|
||||
arrayDestructuring: false,
|
||||
arrowParameter: false,
|
||||
memberVariableDeclaration: false,
|
||||
objectDestructuring: false,
|
||||
parameter: false,
|
||||
propertyDeclaration: true,
|
||||
variableDeclaration: false,
|
||||
variableDeclarationIgnoreFunction: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'@typescript-eslint/unified-signatures': 'off',
|
||||
'brace-style': 'off',
|
||||
'@typescript-eslint/brace-style': 'error',
|
||||
'comma-spacing': 'off',
|
||||
'@typescript-eslint/comma-spacing': 'error',
|
||||
'default-param-last': 'off',
|
||||
'@typescript-eslint/default-param-last': 'error',
|
||||
'dot-notation': 'error',
|
||||
'@typescript-eslint/dot-notation': 'off',
|
||||
'func-call-spacing': 'off',
|
||||
'@typescript-eslint/func-call-spacing': 'error',
|
||||
indent: 'off',
|
||||
'@typescript-eslint/indent': 'off',
|
||||
'@typescript-eslint/init-declarations': 'off',
|
||||
'keyword-spacing': 'off',
|
||||
'@typescript-eslint/keyword-spacing': 'error',
|
||||
'lines-between-class-members': 'off',
|
||||
'@typescript-eslint/lines-between-class-members': [
|
||||
'error',
|
||||
'always',
|
||||
{
|
||||
exceptAfterSingleLine: true,
|
||||
exceptAfterOverload: true,
|
||||
},
|
||||
],
|
||||
'no-array-constructor': 'off',
|
||||
'@typescript-eslint/no-array-constructor': 'error',
|
||||
'no-dupe-class-members': 'off',
|
||||
'@typescript-eslint/no-dupe-class-members': 'error',
|
||||
'no-extra-parens': 'off',
|
||||
'@typescript-eslint/no-extra-parens': 'off',
|
||||
'no-extra-semi': 'off',
|
||||
'@typescript-eslint/no-extra-semi': 'error',
|
||||
'no-invalid-this': 'off',
|
||||
'@typescript-eslint/no-invalid-this': 'error',
|
||||
'no-loss-of-precision': 'off',
|
||||
'@typescript-eslint/no-loss-of-precision': 'error',
|
||||
'no-magic-numbers': 'off',
|
||||
'@typescript-eslint/no-magic-numbers': [
|
||||
'off',
|
||||
{
|
||||
ignoreArrayIndexes: true,
|
||||
ignoreDefaultValues: true,
|
||||
enforceConst: true,
|
||||
ignoreEnums: true,
|
||||
ignoreNumericLiteralTypes: true,
|
||||
ignoreReadonlyClassProperties: true,
|
||||
},
|
||||
],
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': [
|
||||
'error',
|
||||
{
|
||||
builtinGlobals: true,
|
||||
},
|
||||
],
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': [
|
||||
'error',
|
||||
{
|
||||
ignoreTypeValueShadow: false,
|
||||
},
|
||||
],
|
||||
'no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'no-useless-constructor': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': 'error',
|
||||
quotes: 'off',
|
||||
'@typescript-eslint/quotes': [
|
||||
'error',
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
allowTemplateLiterals: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/require-await': 'off',
|
||||
'@typescript-eslint/return-await': 'off',
|
||||
semi: 'off',
|
||||
'@typescript-eslint/semi': ['error', 'never'],
|
||||
'space-before-function-paren': 'off',
|
||||
'@typescript-eslint/space-before-function-paren': [
|
||||
'error',
|
||||
{
|
||||
anonymous: 'never',
|
||||
named: 'never',
|
||||
asyncArrow: 'always',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
4
examples/auth/cms/nodemon.json
Normal file
4
examples/auth/cms/nodemon.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"ext": "ts",
|
||||
"exec": "ts-node src/server.ts"
|
||||
}
|
||||
40
examples/auth/cms/package.json
Normal file
40
examples/auth/cms/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "payload-example-auth",
|
||||
"description": "Payload CMS authentication example.",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/server.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "cross-env PAYLOAD_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
||||
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||
"build:server": "tsc",
|
||||
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
|
||||
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "^1.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.9",
|
||||
"@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-airbnb-base": "^14.2.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"nodemon": "^2.0.6",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
60
examples/auth/cms/src/collections/Users.ts
Normal file
60
examples/auth/cms/src/collections/Users.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import adminsAndUser from './access/adminsAndUser'
|
||||
import { admins } from './access/admins'
|
||||
import { anyone } from './access/anyone'
|
||||
import { checkRole } from './access/checkRole'
|
||||
import { loginAfterCreate } from './hooks/loginAfterCreate'
|
||||
import { protectRoles } from './hooks/protectRoles'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: {
|
||||
tokenExpiration: 28800, // 8 hours
|
||||
cookies: {
|
||||
sameSite: 'none',
|
||||
secure: true,
|
||||
domain: process.env.COOKIE_DOMAIN,
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
access: {
|
||||
read: adminsAndUser,
|
||||
create: anyone,
|
||||
update: adminsAndUser,
|
||||
delete: admins,
|
||||
admin: ({ req: { user } }) => checkRole(['admin'], user),
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [loginAfterCreate],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'firstName',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'lastName',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
hooks: {
|
||||
beforeChange: [protectRoles],
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
value: 'user',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
4
examples/auth/cms/src/collections/access/admins.ts
Normal file
4
examples/auth/cms/src/collections/access/admins.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { Access } from 'payload/config'
|
||||
import { checkRole } from './checkRole'
|
||||
|
||||
export const admins: Access = ({ req: { user } }) => checkRole(['admin'], user)
|
||||
18
examples/auth/cms/src/collections/access/adminsAndUser.ts
Normal file
18
examples/auth/cms/src/collections/access/adminsAndUser.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Access } from 'payload/config'
|
||||
import { checkRole } from './checkRole'
|
||||
|
||||
const adminsAndUser: Access = ({ req: { user } }) => {
|
||||
if (user) {
|
||||
if (checkRole(['admin'], user)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export default adminsAndUser
|
||||
3
examples/auth/cms/src/collections/access/anyone.ts
Normal file
3
examples/auth/cms/src/collections/access/anyone.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { Access } from 'payload/config'
|
||||
|
||||
export const anyone: Access = () => true
|
||||
16
examples/auth/cms/src/collections/access/checkRole.ts
Normal file
16
examples/auth/cms/src/collections/access/checkRole.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { User } from '../../payload-types'
|
||||
|
||||
export const checkRole = (allRoles: User['roles'] = [], user: User = undefined): boolean => {
|
||||
if (user) {
|
||||
if (
|
||||
allRoles.some(role => {
|
||||
return user?.roles?.some(individualRole => {
|
||||
return individualRole === role
|
||||
})
|
||||
})
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
29
examples/auth/cms/src/collections/hooks/loginAfterCreate.ts
Normal file
29
examples/auth/cms/src/collections/hooks/loginAfterCreate.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { AfterChangeHook } from 'payload/dist/collections/config/types'
|
||||
|
||||
export const loginAfterCreate: AfterChangeHook = async ({
|
||||
doc,
|
||||
req,
|
||||
req: { payload, body = {}, res },
|
||||
operation,
|
||||
}) => {
|
||||
if (operation === 'create') {
|
||||
const { email, password } = body
|
||||
|
||||
if (email && password) {
|
||||
const { user, token } = await payload.login({
|
||||
collection: 'users',
|
||||
data: { email, password },
|
||||
req,
|
||||
res,
|
||||
})
|
||||
|
||||
return {
|
||||
...doc,
|
||||
token,
|
||||
user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
15
examples/auth/cms/src/collections/hooks/protectRoles.ts
Normal file
15
examples/auth/cms/src/collections/hooks/protectRoles.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { User } from 'payload/generated-types'
|
||||
import type { FieldHook } from 'payload/types'
|
||||
|
||||
// ensure there is always a `user` role
|
||||
// do not let non-admins change roles
|
||||
// eslint-disable-next-line consistent-return
|
||||
export const protectRoles: FieldHook<User & { id: string }> = async ({ req, data }) => {
|
||||
const isAdmin = req.user?.roles.includes('admin') || data.email === 'dev@payloadcms.com' // for the seed script
|
||||
|
||||
if (!isAdmin) {
|
||||
return ['user']
|
||||
}
|
||||
|
||||
return [...(data?.roles || []), 'user']
|
||||
}
|
||||
27
examples/auth/cms/src/payload-types.ts
Normal file
27
examples/auth/cms/src/payload-types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
users: User;
|
||||
};
|
||||
globals: {};
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
roles?: ('admin' | 'user')[];
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
password?: string;
|
||||
}
|
||||
12
examples/auth/cms/src/payload.config.ts
Normal file
12
examples/auth/cms/src/payload.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { buildConfig } from 'payload/config'
|
||||
import path from 'path'
|
||||
import { Users } from './collections/Users'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Users],
|
||||
cors: [process.env.PAYLOAD_PUBLIC_SERVER_URL, process.env.PAYLOAD_PUBLIC_SITE_URL],
|
||||
csrf: [process.env.PAYLOAD_PUBLIC_SERVER_URL, process.env.PAYLOAD_PUBLIC_SITE_URL],
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
12
examples/auth/cms/src/seed/index.ts
Normal file
12
examples/auth/cms/src/seed/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
export const seed = async (payload: Payload): Promise<void> => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'test',
|
||||
roles: ['admin'],
|
||||
},
|
||||
})
|
||||
}
|
||||
35
examples/auth/cms/src/server.ts
Normal file
35
examples/auth/cms/src/server.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import payload from 'payload'
|
||||
import { seed } from './seed'
|
||||
|
||||
// eslint-disable-next-line
|
||||
require('dotenv').config({
|
||||
path: path.resolve(__dirname, '../.env'),
|
||||
})
|
||||
|
||||
const app = express()
|
||||
|
||||
app.get('/', (_, res) => {
|
||||
res.redirect('/admin')
|
||||
})
|
||||
|
||||
const start = async (): Promise<void> => {
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
mongoURL: process.env.MONGODB_URI,
|
||||
express: app,
|
||||
onInit: () => {
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
|
||||
},
|
||||
})
|
||||
|
||||
if (process.env.PAYLOAD_SEED === 'true') {
|
||||
payload.logger.info('---- SEEDING DATABASE ----')
|
||||
await seed(payload)
|
||||
}
|
||||
|
||||
app.listen(8000)
|
||||
}
|
||||
|
||||
start()
|
||||
34
examples/auth/cms/tsconfig.json
Normal file
34
examples/auth/cms/tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"payload/generated-types": ["./src/payload-types.ts"],
|
||||
"node_modules/*": ["./node_modules/*"]
|
||||
},
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
],
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
}
|
||||
6909
examples/auth/cms/yarn.lock
Normal file
6909
examples/auth/cms/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
1
examples/auth/nextjs/.env.example
Normal file
1
examples/auth/nextjs/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_CMS_URL=http://localhost:8000
|
||||
4
examples/auth/nextjs/.eslintrc.js
Normal file
4
examples/auth/nextjs/.eslintrc.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['./eslint-config'],
|
||||
}
|
||||
6
examples/auth/nextjs/.gitignore
vendored
Normal file
6
examples/auth/nextjs/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.next
|
||||
dist
|
||||
build
|
||||
node_modules
|
||||
.env
|
||||
package-lock.json
|
||||
8
examples/auth/nextjs/.prettierrc.js
Normal file
8
examples/auth/nextjs/.prettierrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: "typescript",
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
arrowParens: "avoid",
|
||||
};
|
||||
35
examples/auth/nextjs/README.md
Normal file
35
examples/auth/nextjs/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Auth Example Front-End
|
||||
|
||||
This is a [Next.js](https://nextjs.org/) app made explicitly for Payload's [Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth/cms).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Payload CMS
|
||||
|
||||
First you'll need a running CMS. If you have not done so already, open up the `cms` folder alongside this example and follow the setup instructions. Take note of your server URL, you'll need this in the next step.
|
||||
|
||||
### Next.js App
|
||||
|
||||
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
|
||||
5. `open http://localhost:3000` to see the result
|
||||
|
||||
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within your CMS. See the [Auth Example CMS](https://github.com/payloadcms/payload/tree/master/examples/auth/cms) for full details.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about PayloadCMS and Next.js, take a look at the following resources:
|
||||
|
||||
- [Payload CMS Documentation](https://payloadcms.com/docs) - learn about Payload CMS features and API.
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Payload CMS GitHub repository](https://github.com/payloadcms/payload/) as well as [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Payload CMS deployment documentaton](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
82
examples/auth/nextjs/components/Auth/index.tsx
Normal file
82
examples/auth/nextjs/components/Auth/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import { User } from '../../payload-types'
|
||||
|
||||
type Login = (args: { email: string; password: string }) => Promise<void> // eslint-disable-line no-unused-vars
|
||||
|
||||
type Logout = () => Promise<void>
|
||||
|
||||
type AuthContext = {
|
||||
user?: User | null
|
||||
setUser: (user: User | null) => void // eslint-disable-line no-unused-vars
|
||||
logout: Logout
|
||||
login: Login
|
||||
}
|
||||
|
||||
const Context = createContext({} as AuthContext)
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>()
|
||||
|
||||
const login = useCallback<Login>(async args => {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/login`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(args),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const json = await res.json()
|
||||
setUser(json.user)
|
||||
} else {
|
||||
throw new Error('Invalid login')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const logout = useCallback<Logout>(async () => {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/logout`, {
|
||||
method: 'POST',
|
||||
// Make sure to include cookies with fetch
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
setUser(null)
|
||||
} else {
|
||||
throw new Error('There was a problem while logging out.')
|
||||
}
|
||||
}, [])
|
||||
|
||||
// On mount, get user and set
|
||||
useEffect(() => {
|
||||
const fetchMe = async () => {
|
||||
const result = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/me`, {
|
||||
// Make sure to include cookies with fetch
|
||||
credentials: 'include',
|
||||
}).then(req => req.json())
|
||||
setUser(result.user || null)
|
||||
}
|
||||
|
||||
fetchMe()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
value={{
|
||||
user,
|
||||
setUser,
|
||||
login,
|
||||
logout,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
type UseAuth<T = User> = () => AuthContext // eslint-disable-line no-unused-vars
|
||||
|
||||
export const useAuth: UseAuth = () => useContext(Context)
|
||||
7
examples/auth/nextjs/components/Gutter/index.module.scss
Normal file
7
examples/auth/nextjs/components/Gutter/index.module.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.gutterLeft {
|
||||
padding-left: var(--gutter-h);
|
||||
}
|
||||
|
||||
.gutterRight {
|
||||
padding-right: var(--gutter-h);
|
||||
}
|
||||
28
examples/auth/nextjs/components/Gutter/index.tsx
Normal file
28
examples/auth/nextjs/components/Gutter/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { forwardRef, Ref } from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
left?: boolean
|
||||
right?: boolean
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
ref?: Ref<HTMLDivElement>
|
||||
}
|
||||
|
||||
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const { left = true, right = true, className, children } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={[left && classes.gutterLeft, right && classes.gutterRight, className]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Gutter.displayName = 'Gutter'
|
||||
18
examples/auth/nextjs/components/Header/index.module.scss
Normal file
18
examples/auth/nextjs/components/Header/index.module.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
@use '../../css/queries.scss' as *;
|
||||
|
||||
.header {
|
||||
padding: var(--base) 0;
|
||||
z-index: var(--header-z-index);
|
||||
}
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nav {
|
||||
a {
|
||||
text-decoration: none;
|
||||
margin-left: var(--base);
|
||||
}
|
||||
}
|
||||
50
examples/auth/nextjs/components/Header/index.tsx
Normal file
50
examples/auth/nextjs/components/Header/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useAuth } from '../Auth'
|
||||
import { Gutter } from '../Gutter'
|
||||
import { Logo } from '../Logo'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type HeaderBarProps = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const HeaderBar: React.FC<HeaderBarProps> = ({ children }) => {
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<Gutter className={classes.wrap}>
|
||||
<Link href="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
{children}
|
||||
</Gutter>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export const Header = () => {
|
||||
const { user } = useAuth()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeaderBar>
|
||||
<nav className={classes.nav}>
|
||||
{!user && (
|
||||
<Fragment>
|
||||
<Link href="/login">Login</Link>
|
||||
<Link href="/create-account">Create Account</Link>
|
||||
</Fragment>
|
||||
)}
|
||||
{user && (
|
||||
<Fragment>
|
||||
<Link href="/account">Account</Link>
|
||||
<Link href="/logout">Logout</Link>
|
||||
</Fragment>
|
||||
)}
|
||||
</nav>
|
||||
</HeaderBar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
23
examples/auth/nextjs/components/Input/index.module.css
Normal file
23
examples/auth/nextjs/components/Input/index.module.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.input {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.input input {
|
||||
width: 100%;
|
||||
font-family: system-ui;
|
||||
border-radius: 0;
|
||||
box-shadow: 0;
|
||||
border: 1px solid #d8d8d8;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 5px;
|
||||
color: red;
|
||||
}
|
||||
25
examples/auth/nextjs/components/Input/index.tsx
Normal file
25
examples/auth/nextjs/components/Input/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { FieldValues, UseFormRegister } from 'react-hook-form';
|
||||
|
||||
import classes from './index.module.css';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
label: string;
|
||||
register: UseFormRegister<FieldValues & any>;
|
||||
required?: boolean;
|
||||
error: any;
|
||||
type?: 'text' | 'number' | 'password';
|
||||
};
|
||||
|
||||
export const Input: React.FC<Props> = ({ name, label, required, register, error, type = 'text' }) => {
|
||||
return (
|
||||
<div className={classes.input}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<input {...{ type }} {...register(name, { required })} />
|
||||
{error && <div className={classes.error}>This field is required</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
48
examples/auth/nextjs/components/Logo/index.tsx
Normal file
48
examples/auth/nextjs/components/Logo/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
|
||||
export const Logo: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
width="123"
|
||||
height="29"
|
||||
viewBox="0 0 123 29"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M34.7441 22.9997H37.2741V16.3297H41.5981C44.7031 16.3297 46.9801 14.9037 46.9801 11.4537C46.9801 8.00369 44.7031 6.55469 41.5981 6.55469H34.7441V22.9997ZM37.2741 14.1447V8.73969H41.4831C43.3921 8.73969 44.3581 9.59069 44.3581 11.4537C44.3581 13.2937 43.3921 14.1447 41.4831 14.1447H37.2741Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M51.3652 23.3217C53.2742 23.3217 54.6082 22.5627 55.3672 21.3437H55.4132C55.5512 22.6777 56.1492 23.1147 57.2762 23.1147C57.6442 23.1147 58.0352 23.0687 58.4262 22.9767V21.5967C58.2882 21.6197 58.2192 21.6197 58.1502 21.6197C57.7132 21.6197 57.5982 21.1827 57.5982 20.3317V14.9497C57.5982 11.9137 55.6662 10.9017 53.2512 10.9017C49.6632 10.9017 48.1912 12.6727 48.0762 14.9267H50.3762C50.4912 13.3627 51.1122 12.7187 53.1592 12.7187C54.8842 12.7187 55.3902 13.4317 55.3902 14.2827C55.3902 15.4327 54.2632 15.6627 52.4232 16.0077C49.5022 16.5597 47.5242 17.3417 47.5242 19.9637C47.5242 21.9647 49.0192 23.3217 51.3652 23.3217ZM49.8702 19.8027C49.8702 18.5837 50.7442 18.0087 52.8142 17.5947C54.0102 17.3417 55.0222 17.0887 55.3902 16.7437V18.4227C55.3902 20.4697 53.8952 21.5047 51.8712 21.5047C50.4682 21.5047 49.8702 20.9067 49.8702 19.8027Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M61.4996 27.1167C63.3166 27.1167 64.4436 26.1737 65.5706 23.2757L70.2166 11.2697H67.8476L64.6276 20.2397H64.5816L61.1546 11.2697H58.6936L63.4316 22.8847C62.9716 24.7247 61.9136 25.1847 61.0166 25.1847C60.6486 25.1847 60.4416 25.1617 60.0506 25.1157V26.9557C60.6486 27.0707 60.9936 27.1167 61.4996 27.1167Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path d="M71.5939 22.9997H73.8479V6.55469H71.5939V22.9997Z" fill="black" />
|
||||
<path
|
||||
d="M81.6221 23.3447C85.2791 23.3447 87.4871 20.7917 87.4871 17.1117C87.4871 13.4547 85.2791 10.9017 81.6451 10.9017C77.9651 10.9017 75.7571 13.4777 75.7571 17.1347C75.7571 20.8147 77.9651 23.3447 81.6221 23.3447ZM78.1031 17.1347C78.1031 14.6737 79.2071 12.7877 81.6451 12.7877C84.0371 12.7877 85.1411 14.6737 85.1411 17.1347C85.1411 19.5727 84.0371 21.4817 81.6451 21.4817C79.2071 21.4817 78.1031 19.5727 78.1031 17.1347Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M92.6484 23.3217C94.5574 23.3217 95.8914 22.5627 96.6504 21.3437H96.6964C96.8344 22.6777 97.4324 23.1147 98.5594 23.1147C98.9274 23.1147 99.3184 23.0687 99.7094 22.9767V21.5967C99.5714 21.6197 99.5024 21.6197 99.4334 21.6197C98.9964 21.6197 98.8814 21.1827 98.8814 20.3317V14.9497C98.8814 11.9137 96.9494 10.9017 94.5344 10.9017C90.9464 10.9017 89.4744 12.6727 89.3594 14.9267H91.6594C91.7744 13.3627 92.3954 12.7187 94.4424 12.7187C96.1674 12.7187 96.6734 13.4317 96.6734 14.2827C96.6734 15.4327 95.5464 15.6627 93.7064 16.0077C90.7854 16.5597 88.8074 17.3417 88.8074 19.9637C88.8074 21.9647 90.3024 23.3217 92.6484 23.3217ZM91.1534 19.8027C91.1534 18.5837 92.0274 18.0087 94.0974 17.5947C95.2934 17.3417 96.3054 17.0887 96.6734 16.7437V18.4227C96.6734 20.4697 95.1784 21.5047 93.1544 21.5047C91.7514 21.5047 91.1534 20.9067 91.1534 19.8027Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M106.181 23.3217C108.021 23.3217 109.148 22.4477 109.792 21.6197H109.838V22.9997H112.092V6.55469H109.838V12.6957H109.792C109.148 11.7757 108.021 10.9247 106.181 10.9247C103.191 10.9247 100.914 13.2707 100.914 17.1347C100.914 20.9987 103.191 23.3217 106.181 23.3217ZM103.26 17.1347C103.26 14.8347 104.341 12.8107 106.549 12.8107C108.573 12.8107 109.815 14.4667 109.815 17.1347C109.815 19.7797 108.573 21.4587 106.549 21.4587C104.341 21.4587 103.26 19.4347 103.26 17.1347Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M12.2464 2.33838L22.2871 8.83812V21.1752L14.7265 25.8854V13.5484L4.67383 7.05725L12.2464 2.33838Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path d="M11.477 25.2017V15.5747L3.90039 20.2936L11.477 25.2017Z" fill="black" />
|
||||
<path
|
||||
d="M120.442 6.30273C119.086 6.30273 117.998 7.29978 117.998 8.75952C117.998 10.2062 119.086 11.1968 120.442 11.1968C121.791 11.1968 122.879 10.2062 122.879 8.75952C122.879 7.29978 121.791 6.30273 120.442 6.30273ZM120.442 10.7601C119.34 10.7601 118.48 9.95207 118.48 8.75952C118.48 7.54742 119.34 6.73935 120.442 6.73935C121.563 6.73935 122.397 7.54742 122.397 8.75952C122.397 9.95207 121.563 10.7601 120.442 10.7601ZM120.52 8.97457L121.048 9.9651H121.641L121.041 8.86378C121.367 8.72042 121.511 8.45975 121.511 8.17302C121.511 7.49528 121.054 7.36495 120.285 7.36495H119.49V9.9651H120.025V8.97457H120.52ZM120.37 7.78853C120.729 7.78853 120.976 7.86673 120.976 8.17953C120.976 8.43368 120.807 8.56402 120.403 8.56402H120.025V7.78853H120.37Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.richText {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
19
examples/auth/nextjs/components/RichText/index.tsx
Normal file
19
examples/auth/nextjs/components/RichText/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
import serialize from './serialize'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
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
|
||||
92
examples/auth/nextjs/components/RichText/serialize.tsx
Normal file
92
examples/auth/nextjs/components/RichText/serialize.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import escapeHTML from 'escape-html'
|
||||
import { Text } from 'slate'
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
type Children = Leaf[]
|
||||
|
||||
type Leaf = {
|
||||
type: string
|
||||
value?: {
|
||||
url: string
|
||||
alt: string
|
||||
}
|
||||
children?: Children
|
||||
url?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const serialize = (children: Children): React.ReactElement[] =>
|
||||
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 style={{ textDecoration: 'underline' }} key={i}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (node.strikethrough) {
|
||||
text = (
|
||||
<span style={{ textDecoration: 'line-through' }} key={i}>
|
||||
{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 'quote':
|
||||
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
|
||||
129
examples/auth/nextjs/css/app.scss
Normal file
129
examples/auth/nextjs/css/app.scss
Normal file
@@ -0,0 +1,129 @@
|
||||
@use './queries.scss' as *;
|
||||
@use './colors.scss' as *;
|
||||
@use './type.scss' as *;
|
||||
|
||||
:root {
|
||||
--breakpoint-xs-width : #{$breakpoint-xs-width};
|
||||
--breakpoint-s-width : #{$breakpoint-s-width};
|
||||
--breakpoint-m-width : #{$breakpoint-m-width};
|
||||
--breakpoint-l-width : #{$breakpoint-l-width};
|
||||
--scrollbar-width: 17px;
|
||||
|
||||
--base: 24px;
|
||||
--font-body: system-ui;
|
||||
--font-mono: 'Roboto Mono', monospace;
|
||||
|
||||
--gutter-h: 180px;
|
||||
--block-padding: 120px;
|
||||
|
||||
--header-z-index: 100;
|
||||
--modal-z-index: 90;
|
||||
|
||||
@include large-break {
|
||||
--gutter-h: 144px;
|
||||
--block-padding: 96px;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
--gutter-h: 24px;
|
||||
--block-padding: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// GLOBAL STYLES
|
||||
/////////////////////////////
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
@extend %body;
|
||||
background: var(--color-white);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
color: var(--color-black);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--color-green);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--color-green);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
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) * .75) 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: var(--base);
|
||||
margin: 0 0 var(--base);
|
||||
}
|
||||
|
||||
a {
|
||||
color: currentColor;
|
||||
|
||||
&:focus {
|
||||
opacity: .8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: .7;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
10
examples/auth/nextjs/css/colors.scss
Normal file
10
examples/auth/nextjs/css/colors.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
:root {
|
||||
--color-red: rgb(255,0,0);
|
||||
--color-green: rgb(178, 255, 214);
|
||||
--color-white: rgb(255, 255, 255);
|
||||
--color-dark-gray: rgb(51,52,52);
|
||||
--color-mid-gray: rgb(196,196,196);
|
||||
--color-gray: rgb(212,212,212);
|
||||
--color-light-gray: rgb(244,244,244);
|
||||
--color-black: rgb(0, 0, 0);
|
||||
}
|
||||
2
examples/auth/nextjs/css/common.scss
Normal file
2
examples/auth/nextjs/css/common.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@forward './queries.scss';
|
||||
@forward './type.scss';
|
||||
32
examples/auth/nextjs/css/queries.scss
Normal file
32
examples/auth/nextjs/css/queries.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
$breakpoint-xs-width: 400px;
|
||||
$breakpoint-s-width: 768px;
|
||||
$breakpoint-m-width: 1024px;
|
||||
$breakpoint-l-width: 1440px;
|
||||
|
||||
////////////////////////////
|
||||
// MEDIA QUERIES
|
||||
/////////////////////////////
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
168
examples/auth/nextjs/css/type.scss
Normal file
168
examples/auth/nextjs/css/type.scss
Normal file
@@ -0,0 +1,168 @@
|
||||
@use 'queries' as *;
|
||||
|
||||
/////////////////////////////
|
||||
// HEADINGS
|
||||
/////////////////////////////
|
||||
|
||||
%h1,
|
||||
%h2,
|
||||
%h3,
|
||||
%h4,
|
||||
%h5,
|
||||
%h6 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
%h1 {
|
||||
margin: 50px 0;
|
||||
font-size: 84px;
|
||||
line-height: 1;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 70px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin: 24px 0;
|
||||
font-size: 36px;
|
||||
line-height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
%h2 {
|
||||
margin: 32px 0;
|
||||
font-size: 56px;
|
||||
line-height: 1;
|
||||
|
||||
@include mid-break {
|
||||
margin: 36px 0;
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin: 24px 0;
|
||||
font-size: 28px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
%h3 {
|
||||
margin: 28px 0;
|
||||
font-size: 48px;
|
||||
line-height: 56px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 40px;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin: 24px 0;
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
%h4 {
|
||||
margin: 24px 0;
|
||||
font-size: 40px;
|
||||
line-height: 48px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 33px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin: 20px 0;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
%h5 {
|
||||
margin: 20px 0;
|
||||
font-size: 32px;
|
||||
line-height: 42px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 26px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin: 16px 0;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
%h6 {
|
||||
margin: 20px 0;
|
||||
font-size: 24px;
|
||||
line-height: 28px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
margin: 16px 0;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// TYPE STYLES
|
||||
/////////////////////////////
|
||||
|
||||
%body {
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
%large-body {
|
||||
font-size: 25px;
|
||||
line-height: 32px;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
%label {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
|
||||
@include mid-break {
|
||||
font-size: 13px;
|
||||
letter-spacing: 2.75px;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: 2.625px;
|
||||
}
|
||||
}
|
||||
14
examples/auth/nextjs/eslint-config/constants.js
Normal file
14
examples/auth/nextjs/eslint-config/constants.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const TEST_FILE_PATTERNS = [
|
||||
'**/**.it-test.ts',
|
||||
'**/**.test.ts',
|
||||
'**/**.spec.ts',
|
||||
'**/__mocks__/**.ts',
|
||||
'**/test/**.ts',
|
||||
]
|
||||
|
||||
const CODE_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.scss', '.json']
|
||||
|
||||
module.exports = {
|
||||
TEST_FILE_PATTERNS,
|
||||
CODE_FILE_EXTENSIONS,
|
||||
}
|
||||
20
examples/auth/nextjs/eslint-config/index.js
Normal file
20
examples/auth/nextjs/eslint-config/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
'plugin:@next/next/recommended',
|
||||
require.resolve('./rules/typescript.js'),
|
||||
require.resolve('./rules/import.js'),
|
||||
require.resolve('./rules/prettier.js'),
|
||||
require.resolve('./rules/style.js'),
|
||||
require.resolve('./rules/react.js'),
|
||||
],
|
||||
env: {
|
||||
es6: true,
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
globals: {
|
||||
NodeJS: true,
|
||||
},
|
||||
}
|
||||
6
examples/auth/nextjs/eslint-config/rules/filenames.js
Normal file
6
examples/auth/nextjs/eslint-config/rules/filenames.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ['filenames'],
|
||||
rules: {
|
||||
'filenames/match-regex': ['error', '^[a-z0-9-.]+$', true],
|
||||
},
|
||||
}
|
||||
52
examples/auth/nextjs/eslint-config/rules/import.js
Normal file
52
examples/auth/nextjs/eslint-config/rules/import.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const { CODE_FILE_EXTENSIONS } = require('../constants')
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
},
|
||||
extends: ['plugin:import/errors', 'plugin:import/warnings', 'plugin:import/typescript'],
|
||||
plugins: ['import', 'simple-import-sort'],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: {
|
||||
extensions: CODE_FILE_EXTENSIONS,
|
||||
},
|
||||
},
|
||||
'import/extensions': CODE_FILE_EXTENSIONS,
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'import/no-unresolved': 'off',
|
||||
'import/no-default-export': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
ts: 'never',
|
||||
tsx: 'never',
|
||||
js: 'never',
|
||||
jsx: 'never',
|
||||
},
|
||||
],
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
['^react', '^@?\\w'],
|
||||
['^(@components|@root|@utils|@scss|@hooks)(/.*|$)', '^\\.((?!.scss).)*$'],
|
||||
['^[^.]'],
|
||||
],
|
||||
},
|
||||
],
|
||||
'simple-import-sort/exports': 'error',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/named': 'error',
|
||||
'import/no-relative-packages': 'warn',
|
||||
'import/no-import-module-exports': 'warn',
|
||||
'import/no-cycle': 'warn',
|
||||
'import/no-duplicates': 'error',
|
||||
},
|
||||
}
|
||||
7
examples/auth/nextjs/eslint-config/rules/prettier.js
Normal file
7
examples/auth/nextjs/eslint-config/rules/prettier.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: ['prettier'],
|
||||
extends: ['plugin:prettier/recommended'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
}
|
||||
3
examples/auth/nextjs/eslint-config/rules/react.js
vendored
Normal file
3
examples/auth/nextjs/eslint-config/rules/react.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: ['plugin:react-hooks/recommended'],
|
||||
}
|
||||
15
examples/auth/nextjs/eslint-config/rules/style.js
Normal file
15
examples/auth/nextjs/eslint-config/rules/style.js
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'prefer-named-exports': 'off',
|
||||
'prefer-destructuring': 'off',
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
'class-methods-use-this': 'off',
|
||||
'function-paren-newline': ['error', 'consistent'],
|
||||
'eol-last': ['error', 'always'],
|
||||
'no-restricted-syntax': 'off',
|
||||
'no-await-in-loop': 'off',
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
'space-infix-ops': 'off',
|
||||
'@typescript-eslint/space-infix-ops': 'warn',
|
||||
},
|
||||
}
|
||||
322
examples/auth/nextjs/eslint-config/rules/typescript.js
Normal file
322
examples/auth/nextjs/eslint-config/rules/typescript.js
Normal file
@@ -0,0 +1,322 @@
|
||||
module.exports = {
|
||||
plugins: ['@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/**.ts', '**/**.d.ts'],
|
||||
rules: {
|
||||
'no-undef': 'off',
|
||||
camelcase: 'off',
|
||||
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
||||
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
|
||||
'@typescript-eslint/await-thenable': 'off',
|
||||
'@typescript-eslint/consistent-type-assertions': [
|
||||
'error',
|
||||
{ assertionStyle: 'as', objectLiteralTypeAssertions: 'allow-as-parameter' },
|
||||
],
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||||
'@typescript-eslint/consistent-type-imports': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'error',
|
||||
{
|
||||
allowExpressions: true,
|
||||
allowTypedFunctionExpressions: true,
|
||||
allowHigherOrderFunctions: true,
|
||||
allowConciseArrowFunctionExpressionsStartingWithVoid: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-member-accessibility': [
|
||||
'error',
|
||||
{ accessibility: 'no-public' },
|
||||
],
|
||||
'@typescript-eslint/member-delimiter-style': [
|
||||
'error',
|
||||
{
|
||||
multiline: {
|
||||
delimiter: 'none',
|
||||
requireLast: true,
|
||||
},
|
||||
singleline: {
|
||||
delimiter: 'semi',
|
||||
requireLast: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/method-signature-style': 'off',
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'off',
|
||||
{
|
||||
selector: 'default',
|
||||
format: ['camelCase'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
format: ['camelCase', 'UPPER_CASE'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: 'typeParameter',
|
||||
format: ['PascalCase'],
|
||||
prefix: ['T', 'U'],
|
||||
},
|
||||
{
|
||||
selector: 'variable',
|
||||
types: ['boolean'],
|
||||
format: ['PascalCase'],
|
||||
prefix: ['is', 'should', 'has', 'can', 'did', 'will'],
|
||||
},
|
||||
{
|
||||
selector: 'interface',
|
||||
format: ['PascalCase'],
|
||||
custom: {
|
||||
regex: '^I[A-Z]',
|
||||
match: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: [
|
||||
'function',
|
||||
'parameter',
|
||||
'property',
|
||||
'parameterProperty',
|
||||
'method',
|
||||
'accessor',
|
||||
],
|
||||
format: ['camelCase'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
{
|
||||
selector: ['class', 'interface', 'typeAlias', 'enum', 'typeParameter'],
|
||||
format: ['PascalCase'],
|
||||
leadingUnderscore: 'forbid',
|
||||
trailingUnderscore: 'forbid',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-base-to-string': 'off',
|
||||
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
|
||||
'@typescript-eslint/no-dynamic-delete': 'error',
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
'@typescript-eslint/no-explicit-any': [
|
||||
'warn',
|
||||
{
|
||||
ignoreRestArgs: true,
|
||||
// enable later
|
||||
fixToUnknown: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-extra-non-null-assertion': 'error',
|
||||
'@typescript-eslint/no-extraneous-class': [
|
||||
'error',
|
||||
{
|
||||
allowConstructorOnly: false,
|
||||
allowEmpty: false,
|
||||
allowStaticOnly: false,
|
||||
allowWithDecorator: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-for-in-array': 'off',
|
||||
'@typescript-eslint/no-implicit-any-catch': [
|
||||
'error',
|
||||
{
|
||||
allowExplicitAny: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-implied-eval': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': [
|
||||
'error',
|
||||
{
|
||||
ignoreParameters: false,
|
||||
ignoreProperties: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-invalid-void-type': [
|
||||
'off',
|
||||
{
|
||||
allowInGenericTypeArguments: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-misused-new': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'@typescript-eslint/no-parameter-properties': 'error',
|
||||
'@typescript-eslint/no-require-imports': 'error',
|
||||
'@typescript-eslint/no-this-alias': 'error',
|
||||
'@typescript-eslint/no-throw-literal': 'off',
|
||||
'@typescript-eslint/no-type-alias': [
|
||||
'off',
|
||||
{
|
||||
allowAliases: 'always',
|
||||
allowCallbacks: 'always',
|
||||
allowConditionalTypes: 'always',
|
||||
allowConstructors: 'never',
|
||||
allowLiterals: 'in-unions-and-intersections',
|
||||
allowMappedTypes: 'always',
|
||||
allowTupleTypes: 'always',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
'@typescript-eslint/no-unnecessary-qualifier': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-arguments': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'error',
|
||||
'@typescript-eslint/prefer-as-const': 'error',
|
||||
'@typescript-eslint/prefer-enum-initializers': 'off',
|
||||
'@typescript-eslint/prefer-for-of': 'error',
|
||||
'@typescript-eslint/prefer-includes': 'off',
|
||||
'@typescript-eslint/prefer-literal-enum-member': 'error',
|
||||
'@typescript-eslint/prefer-namespace-keyword': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/prefer-readonly': 'off',
|
||||
'@typescript-eslint/prefer-readonly-parameter-types': 'off',
|
||||
'@typescript-eslint/prefer-reduce-type-parameter': 'off',
|
||||
'@typescript-eslint/prefer-regexp-exec': 'off',
|
||||
'@typescript-eslint/prefer-string-starts-ends-with': 'off',
|
||||
'@typescript-eslint/prefer-ts-expect-error': 'warn',
|
||||
'@typescript-eslint/promise-function-async': 'off',
|
||||
'@typescript-eslint/require-array-sort-compare': 'off',
|
||||
'@typescript-eslint/restrict-plus-operands': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||
'@typescript-eslint/switch-exhaustiveness-check': 'off',
|
||||
'@typescript-eslint/triple-slash-reference': 'error',
|
||||
'@typescript-eslint/type-annotation-spacing': [
|
||||
'error',
|
||||
{
|
||||
before: false,
|
||||
after: true,
|
||||
overrides: {
|
||||
arrow: {
|
||||
before: true,
|
||||
after: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/typedef': [
|
||||
'error',
|
||||
{
|
||||
arrayDestructuring: false,
|
||||
arrowParameter: false,
|
||||
memberVariableDeclaration: false,
|
||||
objectDestructuring: false,
|
||||
parameter: false,
|
||||
propertyDeclaration: true,
|
||||
variableDeclaration: false,
|
||||
variableDeclarationIgnoreFunction: false,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'@typescript-eslint/unified-signatures': 'off',
|
||||
'brace-style': 'off',
|
||||
'@typescript-eslint/brace-style': 'error',
|
||||
'comma-spacing': 'off',
|
||||
'@typescript-eslint/comma-spacing': 'error',
|
||||
'default-param-last': 'off',
|
||||
'@typescript-eslint/default-param-last': 'error',
|
||||
'dot-notation': 'error',
|
||||
'@typescript-eslint/dot-notation': 'off',
|
||||
'func-call-spacing': 'off',
|
||||
'@typescript-eslint/func-call-spacing': 'error',
|
||||
indent: 'off',
|
||||
'@typescript-eslint/indent': 'off',
|
||||
'@typescript-eslint/init-declarations': 'off',
|
||||
'keyword-spacing': 'off',
|
||||
'@typescript-eslint/keyword-spacing': 'error',
|
||||
'lines-between-class-members': 'off',
|
||||
'@typescript-eslint/lines-between-class-members': [
|
||||
'error',
|
||||
'always',
|
||||
{
|
||||
exceptAfterSingleLine: true,
|
||||
exceptAfterOverload: true,
|
||||
},
|
||||
],
|
||||
'no-array-constructor': 'off',
|
||||
'@typescript-eslint/no-array-constructor': 'error',
|
||||
'no-dupe-class-members': 'off',
|
||||
'@typescript-eslint/no-dupe-class-members': 'error',
|
||||
'no-extra-parens': 'off',
|
||||
'@typescript-eslint/no-extra-parens': 'off',
|
||||
'no-extra-semi': 'off',
|
||||
'@typescript-eslint/no-extra-semi': 'error',
|
||||
'no-invalid-this': 'off',
|
||||
'@typescript-eslint/no-invalid-this': 'error',
|
||||
'no-loss-of-precision': 'off',
|
||||
'@typescript-eslint/no-loss-of-precision': 'error',
|
||||
'no-magic-numbers': 'off',
|
||||
'@typescript-eslint/no-magic-numbers': [
|
||||
'off',
|
||||
{
|
||||
ignoreArrayIndexes: true,
|
||||
ignoreDefaultValues: true,
|
||||
enforceConst: true,
|
||||
ignoreEnums: true,
|
||||
ignoreNumericLiteralTypes: true,
|
||||
ignoreReadonlyClassProperties: true,
|
||||
},
|
||||
],
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': [
|
||||
'error',
|
||||
{
|
||||
builtinGlobals: true,
|
||||
},
|
||||
],
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': [
|
||||
'error',
|
||||
{
|
||||
ignoreTypeValueShadow: false,
|
||||
},
|
||||
],
|
||||
'no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'no-useless-constructor': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': 'error',
|
||||
quotes: 'off',
|
||||
'@typescript-eslint/quotes': [
|
||||
'error',
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
allowTemplateLiterals: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/require-await': 'off',
|
||||
'@typescript-eslint/return-await': 'off',
|
||||
semi: 'off',
|
||||
'@typescript-eslint/semi': ['error', 'never'],
|
||||
'space-before-function-paren': 'off',
|
||||
'@typescript-eslint/space-before-function-paren': [
|
||||
'error',
|
||||
{
|
||||
anonymous: 'never',
|
||||
named: 'never',
|
||||
asyncArrow: 'always',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
5
examples/auth/nextjs/next-env.d.ts
vendored
Normal file
5
examples/auth/nextjs/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
10
examples/auth/nextjs/next.config.js
Normal file
10
examples/auth/nextjs/next.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
images: {
|
||||
domains: ['localhost', process.env.NEXT_PUBLIC_CMS_URL],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
36
examples/auth/nextjs/package.json
Normal file
36
examples/auth/nextjs/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "next-auth-frontend",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"prettier:fix": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-html": "^1.0.3",
|
||||
"next": "^13.1.6",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.34.2",
|
||||
"slate": "^0.82.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^13.1.6",
|
||||
"@types/node": "18.11.3",
|
||||
"@types/react": "18.0.21",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"eslint": "8.25.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"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",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "4.8.4"
|
||||
}
|
||||
}
|
||||
15
examples/auth/nextjs/pages/_app.tsx
Normal file
15
examples/auth/nextjs/pages/_app.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { AppProps } from 'next/app'
|
||||
|
||||
import { AuthProvider } from '../components/Auth'
|
||||
import { Header } from '../components/Header'
|
||||
|
||||
import '../css/app.scss'
|
||||
|
||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Header />
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
16
examples/auth/nextjs/pages/account/index.module.css
Normal file
16
examples/auth/nextjs/pages/account/index.module.css
Normal file
@@ -0,0 +1,16 @@
|
||||
.form {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.success,
|
||||
.error {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
105
examples/auth/nextjs/pages/account/index.tsx
Normal file
105
examples/auth/nextjs/pages/account/index.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useAuth } from '../../components/Auth'
|
||||
import { Gutter } from '../../components/Gutter'
|
||||
import { Input } from '../../components/Input'
|
||||
import classes from './index.module.css'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const Account: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
const { user, setUser } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
if (user) {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/${user.id}`, {
|
||||
// Make sure to include cookies with fetch
|
||||
credentials: 'include',
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json()
|
||||
|
||||
// Update the user in auth state with new values
|
||||
setUser(json.doc)
|
||||
|
||||
// Set success message for user
|
||||
setSuccess('Successfully updated account.')
|
||||
|
||||
// Clear any existing errors
|
||||
setError('')
|
||||
} else {
|
||||
setError('There was a problem updating your account.')
|
||||
}
|
||||
}
|
||||
},
|
||||
[user, setUser],
|
||||
)
|
||||
|
||||
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,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
})
|
||||
}
|
||||
}, [user, reset, router])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof router.query.success === 'string') {
|
||||
setSuccess(router.query.success)
|
||||
}
|
||||
}, [router])
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
<h1>Account</h1>
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
{success && <div className={classes.success}>{success}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
||||
<Input
|
||||
name="email"
|
||||
label="Email Address"
|
||||
required
|
||||
register={register}
|
||||
error={errors.email}
|
||||
/>
|
||||
<Input name="firstName" label="First Name" register={register} error={errors.firstName} />
|
||||
<Input name="lastName" label="Last Name" register={register} error={errors.lastName} />
|
||||
<button type="submit">Update account</button>
|
||||
</form>
|
||||
<Link href="/logout">Log out</Link>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default Account
|
||||
@@ -0,0 +1,7 @@
|
||||
.form {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
102
examples/auth/nextjs/pages/create-account/index.tsx
Normal file
102
examples/auth/nextjs/pages/create-account/index.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useAuth } from '../../components/Auth'
|
||||
import { Gutter } from '../../components/Gutter'
|
||||
import { Input } from '../../components/Input'
|
||||
import classes from './index.module.css'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
password: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const CreateAccount: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
const { login } = useAuth()
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// Automatically log the user in
|
||||
await login({ email: data.email, password: data.password })
|
||||
|
||||
// Set success message for user
|
||||
setSuccess(true)
|
||||
|
||||
// Clear any existing errors
|
||||
setError('')
|
||||
} else {
|
||||
setError('There was a problem creating your account. Please try again later.')
|
||||
}
|
||||
},
|
||||
[login],
|
||||
)
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
{!success && (
|
||||
<React.Fragment>
|
||||
<h1>Create Account</h1>
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
||||
<Input
|
||||
name="email"
|
||||
label="Email Address"
|
||||
required
|
||||
register={register}
|
||||
error={errors.email}
|
||||
/>
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
label="Password"
|
||||
required
|
||||
register={register}
|
||||
error={errors.password}
|
||||
/>
|
||||
<Input
|
||||
name="firstName"
|
||||
label="First Name"
|
||||
register={register}
|
||||
error={errors.firstName}
|
||||
/>
|
||||
<Input name="lastName" label="Last Name" register={register} error={errors.lastName} />
|
||||
<button type="submit">Create account</button>
|
||||
</form>
|
||||
<p>
|
||||
{'Already have an account? '}
|
||||
<Link href="/login">Login</Link>
|
||||
</p>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{success && (
|
||||
<React.Fragment>
|
||||
<h1>Account created successfully</h1>
|
||||
<p>You are now logged in.</p>
|
||||
<Link href="/account">Go to your account</Link>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateAccount
|
||||
32
examples/auth/nextjs/pages/index.tsx
Normal file
32
examples/auth/nextjs/pages/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Gutter } from '../components/Gutter'
|
||||
|
||||
const Home: React.FC = () => {
|
||||
return (
|
||||
<Gutter>
|
||||
<h1>Home</h1>
|
||||
<p>
|
||||
{'This is a '}
|
||||
<Link href="https://nextjs.org/">Next.js</Link>
|
||||
{" app made explicitly for Payload's "}
|
||||
<Link href="https://github.com/payloadcms/payload/tree/master/examples/auth/cms">
|
||||
Auth Example
|
||||
</Link>
|
||||
{". This example demonstrates how to implement Payload's "}
|
||||
<Link href="https://payloadcms.com/docs/authentication/overview">Authentication</Link>
|
||||
{' strategies.'}
|
||||
</p>
|
||||
<p>
|
||||
{'Visit the '}
|
||||
<Link href="/login">Login</Link>
|
||||
{' page to start the authentication flow. Once logged in, you will be redirected to the '}
|
||||
<Link href="/account">Account</Link>
|
||||
{" page which is restricted to user's only."}
|
||||
</p>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
8
examples/auth/nextjs/pages/login/index.module.css
Normal file
8
examples/auth/nextjs/pages/login/index.module.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.form {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
77
examples/auth/nextjs/pages/login/index.tsx
Normal file
77
examples/auth/nextjs/pages/login/index.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useAuth } from '../../components/Auth'
|
||||
import { Gutter } from '../../components/Gutter'
|
||||
import { Input } from '../../components/Input'
|
||||
import classes from './index.module.css'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const router = useRouter()
|
||||
const { login } = useAuth()
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
await login(data)
|
||||
router.push('/account')
|
||||
} catch (_) {
|
||||
setError('There was an error with the credentials provided. Please try again.')
|
||||
}
|
||||
},
|
||||
[login, router],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query.unauthorized) {
|
||||
setError(`To visit the ${router.query.unauthorized} page, you need to be logged in.`)
|
||||
}
|
||||
}, [router])
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
<h1>Log in</h1>
|
||||
<p>
|
||||
To log in, use the email <b>dev@payloadcms.com</b> with the password <b>test</b>.
|
||||
</p>
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
||||
<Input
|
||||
name="email"
|
||||
label="Email Address"
|
||||
required
|
||||
register={register}
|
||||
error={errors.email}
|
||||
/>
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
label="Password"
|
||||
required
|
||||
register={register}
|
||||
error={errors.password}
|
||||
/>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
<Link href="/create-account">Create an account</Link>
|
||||
<br />
|
||||
<Link href="/recover-password">Recover your password</Link>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
||||
4
examples/auth/nextjs/pages/logout/index.module.css
Normal file
4
examples/auth/nextjs/pages/logout/index.module.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
42
examples/auth/nextjs/pages/logout/index.tsx
Normal file
42
examples/auth/nextjs/pages/logout/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useAuth } from '../../components/Auth'
|
||||
import { Gutter } from '../../components/Gutter'
|
||||
import classes from './index.module.css'
|
||||
|
||||
const Logout: 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.')
|
||||
}
|
||||
}
|
||||
|
||||
performLogout()
|
||||
}, [logout])
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
{success && <h1>{success}</h1>}
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<p>
|
||||
{'What would you like to do next? '}
|
||||
<Fragment>
|
||||
{' To log back in, '}
|
||||
<Link href={`/login`}>click here</Link>
|
||||
{'.'}
|
||||
</Fragment>
|
||||
</p>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default Logout
|
||||
@@ -0,0 +1,4 @@
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
76
examples/auth/nextjs/pages/recover-password/index.tsx
Normal file
76
examples/auth/nextjs/pages/recover-password/index.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { Gutter } from '../../components/Gutter'
|
||||
import { Input } from '../../components/Input'
|
||||
import classes from './index.module.css'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
}
|
||||
|
||||
const RecoverPassword: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(async (data: FormData) => {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/forgot-password`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// Set success message for user
|
||||
setSuccess(true)
|
||||
|
||||
// Clear any existing errors
|
||||
setError('')
|
||||
} else {
|
||||
setError(
|
||||
'There was a problem while attempting to send you a password reset email. Please try again later.',
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
{!success && (
|
||||
<React.Fragment>
|
||||
<h1>Recover Password</h1>
|
||||
<p>
|
||||
Please enter your email below. You will receive an email message with instructions on
|
||||
how to reset your password.
|
||||
</p>
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input
|
||||
name="email"
|
||||
label="Email Address"
|
||||
required
|
||||
register={register}
|
||||
error={errors.email}
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</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>
|
||||
)}
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecoverPassword
|
||||
81
examples/auth/nextjs/pages/reset-password/index.tsx
Normal file
81
examples/auth/nextjs/pages/reset-password/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useAuth } from '../../components/Auth'
|
||||
import { Gutter } from '../../components/Gutter'
|
||||
import { Input } from '../../components/Input'
|
||||
import classes from './index.module.css'
|
||||
|
||||
type FormData = {
|
||||
password: string
|
||||
token: string
|
||||
}
|
||||
|
||||
const ResetPassword: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const { login } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
const token = typeof router.query.token === 'string' ? router.query.token : undefined
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/reset-password`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
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 NextJS populates token within router,
|
||||
// reset form with new token value
|
||||
useEffect(() => {
|
||||
reset({ token })
|
||||
}, [reset, token])
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
<h1>Reset Password</h1>
|
||||
<p>Please enter a new password below.</p>
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
label="New Password"
|
||||
required
|
||||
register={register}
|
||||
error={errors.password}
|
||||
/>
|
||||
<input type="hidden" {...register('token')} />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResetPassword
|
||||
27
examples/auth/nextjs/payload-types.ts
Normal file
27
examples/auth/nextjs/payload-types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload CMS.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
users: User;
|
||||
};
|
||||
globals: {};
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
roles?: ('admin' | 'user')[];
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
password?: string;
|
||||
}
|
||||
BIN
examples/auth/nextjs/public/favicon.ico
Normal file
BIN
examples/auth/nextjs/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
30
examples/auth/nextjs/tsconfig.json
Normal file
30
examples/auth/nextjs/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
1879
examples/auth/nextjs/yarn.lock
Normal file
1879
examples/auth/nextjs/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user