merges with master

This commit is contained in:
Jarrod Flesch
2020-07-27 12:38:26 -04:00
271 changed files with 9042 additions and 6899 deletions

View File

@@ -1,22 +1,6 @@
module.exports = { module.exports = {
parser: "babel-eslint", parser: "babel-eslint",
env: { extends: "@trbl",
browser: true,
es6: true,
jest: true,
},
extends: 'airbnb',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: [
'react',
'react-hooks',
],
rules: { rules: {
"import/no-unresolved": [ "import/no-unresolved": [
2, 2,
@@ -25,56 +9,7 @@ module.exports = {
'payload/config', 'payload/config',
'payload/unsanitizedConfig', 'payload/unsanitizedConfig',
] ]
}],
"react/jsx-filename-extension": [
1,
{
"extensions": [
".js",
".jsx"
]
} }
], ],
"no-console": 0,
"camelcase": 0,
"arrow-body-style": 0,
"jsx-a11y/anchor-is-valid": [
"error",
{
"aspects": [
"invalidHref",
"preferButton"
]
}
],
"jsx-a11y/click-events-have-key-events": 0,
"jsx-a11y/label-has-for": [
2,
{
"components": [
"Label"
],
"required": {
"every": [
"id"
]
},
"allowChildren": false
}
],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react/no-array-index-key": 0,
"max-len": 0,
"react/no-danger": 0,
"import/prefer-default-export": 0,
"no-throw-literal": 0,
"react/jsx-max-props-per-line": [
1,
{
"maximum": 1
}
],
"linebreak-style": ["off"]
}, },
}; };

View File

@@ -9,8 +9,8 @@ module.exports = {
{ {
name: 'author', name: 'author',
label: 'Author', label: 'Author',
type: 'text', type: 'relationship',
maxLength: 100, relationTo: 'public-users',
required: true, required: true,
}, },
{ {

View File

@@ -1,7 +1,7 @@
const roles = require('../policies/roles'); const roles = require('../access/roles');
const checkRole = require('../policies/checkRole'); const checkRole = require('../access/checkRole');
const policy = ({ req: { user } }) => { const access = ({ req: { user } }) => {
const result = checkRole(['admin'], user); const result = checkRole(['admin'], user);
return result; return result;
}; };
@@ -12,17 +12,17 @@ module.exports = {
singular: 'Admin', singular: 'Admin',
plural: 'Admins', plural: 'Admins',
}, },
useAsTitle: 'email', access: {
policies: { create: access,
create: policy, read: access,
read: policy, update: access,
update: policy, delete: access,
delete: policy,
admin: () => true, admin: () => true,
}, },
auth: { auth: {
tokenExpiration: 300, tokenExpiration: 7200,
useAPIKey: true, useAPIKey: true,
secureCookie: process.env.NODE_ENV === 'production',
}, },
fields: [ fields: [
{ {
@@ -35,6 +35,27 @@ module.exports = {
saveToJWT: true, saveToJWT: true,
hasMany: true, hasMany: true,
}, },
{
name: 'apiKey',
access: {
read: ({ req: { user } }) => {
if (checkRole(['admin'], user)) {
return true;
}
if (user) {
return {
email: user.email,
};
}
return false;
},
},
},
], ],
timestamps: true, timestamps: true,
admin: {
useAsTitle: 'email',
},
}; };

View File

@@ -1,8 +1,8 @@
const checkRole = require('../policies/checkRole'); const checkRole = require('../access/checkRole');
const Email = require('../content-blocks/Email'); const Email = require('../blocks/Email');
const Quote = require('../content-blocks/Quote'); const Quote = require('../blocks/Quote');
const NumberBlock = require('../content-blocks/Number'); const NumberBlock = require('../blocks/Number');
const CallToAction = require('../content-blocks/CallToAction'); const CallToAction = require('../blocks/CallToAction');
const AllFields = { const AllFields = {
slug: 'all-fields', slug: 'all-fields',
@@ -10,7 +10,9 @@ const AllFields = {
singular: 'All Fields', singular: 'All Fields',
plural: 'All Fields', plural: 'All Fields',
}, },
useAsTitle: 'text', admin: {
useAsTitle: 'text',
},
preview: (doc, token) => { preview: (doc, token) => {
if (doc && doc.text) { if (doc && doc.text) {
return `http://localhost:3000/previewable-posts/${doc.text.value}?preview=true&token=${token}`; return `http://localhost:3000/previewable-posts/${doc.text.value}?preview=true&token=${token}`;
@@ -18,7 +20,7 @@ const AllFields = {
return null; return null;
}, },
policies: { access: {
read: () => true, read: () => true,
}, },
fields: [ fields: [
@@ -29,15 +31,9 @@ const AllFields = {
required: true, required: true,
defaultValue: 'Default Value', defaultValue: 'Default Value',
unique: true, unique: true,
policies: { access: {
create: () => { create: ({ req: { user } }) => checkRole(['admin'], user),
console.log('trying to set text'); update: ({ req: { user } }) => checkRole(['admin'], user),
return false;
},
update: ({ req: { user } }) => {
const result = checkRole(['admin'], user);
return result;
},
read: ({ req: { user } }) => Boolean(user), read: ({ req: { user } }) => Boolean(user),
}, },
}, },
@@ -105,12 +101,6 @@ const AllFields = {
defaultValue: 'option-2', defaultValue: 'option-2',
required: true, required: true,
}, },
{
name: 'checkbox',
type: 'checkbox',
label: 'Checkbox',
position: 'sidebar',
},
{ {
type: 'row', type: 'row',
fields: [ fields: [
@@ -147,9 +137,9 @@ const AllFields = {
], ],
}, },
{ {
type: 'repeater', type: 'array',
label: 'Repeater', label: 'Array',
name: 'repeater', name: 'array',
minRows: 2, minRows: 2,
maxRows: 4, maxRows: 4,
fields: [ fields: [
@@ -157,42 +147,41 @@ const AllFields = {
type: 'row', type: 'row',
fields: [ fields: [
{ {
name: 'repeaterText1', name: 'arrayText1',
label: 'Repeater Text 1', label: 'Array Text 1',
type: 'text', type: 'text',
required: true, required: true,
}, { }, {
name: 'repeaterText2', name: 'arrayText2',
label: 'Repeater Text 2', label: 'Array Text 2',
type: 'text', type: 'text',
required: true, required: true,
policies: { access: {
read: ({ req: { user } }) => Boolean(user), read: ({ req: { user } }) => Boolean(user),
update: ({ req: { user } }) => { update: ({ req: { user } }) => checkRole(['admin'], user),
return checkRole(['admin'], user);
},
}, },
}, },
], ],
}, },
{ {
type: 'text', type: 'text',
name: 'repeaterText3', name: 'arrayText3',
label: 'Repeater Text 3', label: 'Array Text 3',
readOnly: true, admin: {
readOnly: true,
},
}, },
], ],
}, },
{ {
type: 'flexible', type: 'blocks',
label: 'Flexible Content', label: 'Blocks Content',
name: 'flexible', name: 'blocks',
minRows: 2, minRows: 2,
singularLabel: 'Block', singularLabel: 'Block',
blocks: [Email, NumberBlock, Quote, CallToAction], blocks: [Email, NumberBlock, Quote, CallToAction],
localized: true, localized: true,
required: true, required: true,
timestamps: true,
}, },
{ {
type: 'relationship', type: 'relationship',
@@ -218,6 +207,25 @@ const AllFields = {
label: 'Textarea', label: 'Textarea',
name: 'textarea', name: 'textarea',
}, },
{
name: 'slug',
type: 'text',
label: 'Slug',
admin: {
position: 'sidebar',
},
localized: true,
unique: true,
required: true,
},
{
name: 'checkbox',
type: 'checkbox',
label: 'Checkbox',
admin: {
position: 'sidebar',
},
},
], ],
timestamps: true, timestamps: true,
}; };

View File

@@ -0,0 +1,26 @@
const Email = require('../blocks/Email');
const Quote = require('../blocks/Quote');
const NumberBlock = require('../blocks/Number');
const CallToAction = require('../blocks/CallToAction');
module.exports = {
slug: 'blocks',
labels: {
singular: 'Blocks',
plural: 'Blocks',
},
access: {
read: () => true,
},
fields: [
{
name: 'layout',
label: 'Layout Blocks',
singularLabel: 'Block',
type: 'blocks',
blocks: [Email, NumberBlock, Quote, CallToAction],
localized: true,
required: true,
},
],
};

View File

@@ -26,21 +26,25 @@ const Conditions = {
type: 'text', type: 'text',
label: 'Enable Test is checked', label: 'Enable Test is checked',
required: true, required: true,
condition: (_, siblings) => siblings.enableTest === true, admin: {
condition: (_, siblings) => siblings.enableTest === true,
},
}, },
{ {
name: 'orCondition', name: 'orCondition',
type: 'text', type: 'text',
label: 'Number is greater than 20 OR enableTest is checked', label: 'Number is greater than 20 OR enableTest is checked',
required: true, required: true,
condition: (_, siblings) => siblings.number > 20 || siblings.enableTest === true, admin: {
condition: (_, siblings) => siblings.number > 20 || siblings.enableTest === true,
},
}, },
{ {
name: 'nestedConditions', name: 'nestedConditions',
type: 'text', type: 'text',
label: 'Number is either greater than 20 AND enableTest is checked, OR number is less than 20 and enableTest is NOT checked', label: 'Number is either greater than 20 AND enableTest is checked, OR number is less than 20 and enableTest is NOT checked',
condition: (_, siblings) => { admin: {
return (siblings.number > 20 && siblings.enableTest === true) || (siblings.number < 20 && siblings.enableTest === false); condition: (_, siblings) => (siblings.number > 20 && siblings.enableTest === true) || (siblings.number < 20 && siblings.enableTest === false),
}, },
}, },
], ],

View File

@@ -0,0 +1,5 @@
import React from 'react';
const NestedArrayCustomField = () => <div className="nested-array-custom-field">Nested array custom field</div>;
export default NestedArrayCustomField;

View File

@@ -1,5 +0,0 @@
import React from 'react';
const NestedRepeaterCustomField = () => <div className="nested-repeater-custom-field">Nested repeater custom field</div>;
export default NestedRepeaterCustomField;

View File

@@ -6,7 +6,6 @@ module.exports = {
singular: 'Custom Component', singular: 'Custom Component',
plural: 'Custom Components', plural: 'Custom Components',
}, },
useAsTitle: 'title',
fields: [ fields: [
{ {
name: 'title', name: 'title',
@@ -16,11 +15,6 @@ module.exports = {
required: true, required: true,
unique: true, unique: true,
localized: true, localized: true,
hooks: {
beforeCreate: operation => operation.value,
beforeUpdate: operation => operation.value,
afterRead: operation => operation.value,
},
}, },
{ {
name: 'description', name: 'description',
@@ -29,23 +23,27 @@ module.exports = {
height: 100, height: 100,
required: true, required: true,
localized: true, localized: true,
components: { admin: {
field: path.resolve(__dirname, 'components/fields/Description/Field/index.js'), components: {
cell: path.resolve(__dirname, 'components/fields/Description/Cell/index.js'), field: path.resolve(__dirname, 'components/fields/Description/Field/index.js'),
filter: path.resolve(__dirname, 'components/fields/Description/Filter/index.js'), cell: path.resolve(__dirname, 'components/fields/Description/Cell/index.js'),
filter: path.resolve(__dirname, 'components/fields/Description/Filter/index.js'),
},
}, },
}, },
{ {
name: 'repeater', name: 'array',
label: 'Repeater', label: 'Array',
type: 'repeater', type: 'array',
fields: [ fields: [
{ {
type: 'text', type: 'text',
name: 'nestedRepeaterCustomField', name: 'nestedArrayCustomField',
label: 'Nested Repeater Custom Field', label: 'Nested Array Custom Field',
components: { admin: {
field: path.resolve(__dirname, 'components/fields/NestedRepeaterCustomField/Field/index.js'), components: {
field: path.resolve(__dirname, 'components/fields/NestedArrayCustomField/Field/index.js'),
},
}, },
}, },
], ],
@@ -54,16 +52,20 @@ module.exports = {
name: 'group', name: 'group',
label: 'Group', label: 'Group',
type: 'group', type: 'group',
components: { admin: {
field: path.resolve(__dirname, 'components/fields/Group/Field/index.js'), components: {
field: path.resolve(__dirname, 'components/fields/Group/Field/index.js'),
},
}, },
fields: [ fields: [
{ {
type: 'text', type: 'text',
name: 'nestedGroupCustomField', name: 'nestedGroupCustomField',
label: 'Nested Group Custom Field', label: 'Nested Group Custom Field',
components: { admin: {
field: path.resolve(__dirname, 'components/fields/NestedGroupCustomField/Field/index.js'), components: {
field: path.resolve(__dirname, 'components/fields/NestedGroupCustomField/Field/index.js'),
},
}, },
}, },
], ],
@@ -75,8 +77,10 @@ module.exports = {
name: 'nestedText1', name: 'nestedText1',
label: 'Nested Text 1', label: 'Nested Text 1',
type: 'text', type: 'text',
components: { admin: {
field: path.resolve(__dirname, 'components/fields/NestedText1/Field/index.js'), components: {
field: path.resolve(__dirname, 'components/fields/NestedText1/Field/index.js'),
},
}, },
}, { }, {
name: 'nestedText2', name: 'nestedText2',
@@ -87,9 +91,12 @@ module.exports = {
}, },
], ],
timestamps: true, timestamps: true,
components: { admin: {
views: { useAsTitle: 'title',
List: path.resolve(__dirname, 'components/views/List/index.js'), components: {
views: {
List: path.resolve(__dirname, 'components/views/List/index.js'),
},
}, },
}, },
}; };

View File

@@ -1,7 +1,7 @@
const path = require('path'); const path = require('path');
const checkRole = require('../policies/checkRole'); const checkRole = require('../access/checkRole');
const policy = ({ req: { user } }) => { const access = ({ req: { user } }) => {
const isAdmin = checkRole(['admin'], user); const isAdmin = checkRole(['admin'], user);
if (isAdmin) { if (isAdmin) {
@@ -27,13 +27,12 @@ module.exports = {
staticURL: '/files', staticURL: '/files',
staticDir: path.resolve(__dirname, '../files'), staticDir: path.resolve(__dirname, '../files'),
}, },
policies: { access: {
create: () => true, create: () => true,
read: policy, read: access,
update: policy, update: access,
delete: policy, delete: access,
}, },
useAsTitle: 'filename',
fields: [ fields: [
{ {
name: 'type', name: 'type',
@@ -61,4 +60,7 @@ module.exports = {
}, },
], ],
timestamps: true, timestamps: true,
admin: {
useAsTitle: 'filename',
},
}; };

View File

@@ -1,27 +0,0 @@
const Email = require('../content-blocks/Email');
const Quote = require('../content-blocks/Quote');
const NumberBlock = require('../content-blocks/Number');
const CallToAction = require('../content-blocks/CallToAction');
module.exports = {
slug: 'flexible-content',
labels: {
singular: 'Flexible Content',
plural: 'Flexible Content',
},
policies: {
read: () => true,
},
fields: [
{
name: 'layout',
label: 'Layout Blocks',
singularLabel: 'Block',
type: 'flexible',
blocks: [Email, NumberBlock, Quote, CallToAction],
localized: true,
required: true,
},
],
timestamps: true,
};

View File

@@ -5,63 +5,79 @@ module.exports = {
singular: 'Hook', singular: 'Hook',
plural: 'Hooks', plural: 'Hooks',
}, },
useAsTitle: 'title', admin: {
policies: { useAsTitle: 'title',
},
access: {
create: () => true, create: () => true,
read: () => true, read: () => true,
update: () => true, update: () => true,
delete: () => true, delete: () => true,
}, },
hooks: { hooks: {
beforeCreate: (operation) => { beforeCreate: [
if (operation.req.headers.hook === 'beforeCreate') { (operation) => {
operation.req.body.description += '-beforeCreateSuffix'; if (operation.req.headers.hook === 'beforeCreate') {
} operation.req.body.description += '-beforeCreateSuffix';
return operation; }
}, return operation.data;
beforeRead: (operation) => { },
if (operation.req.headers.hook === 'beforeRead') { ],
operation.limit = 1; beforeRead: [
} (operation) => {
return operation; if (operation.req.headers.hook === 'beforeRead') {
}, console.log('before reading Hooks document');
beforeUpdate: (operation) => { }
if (operation.req.headers.hook === 'beforeUpdate') { },
operation.req.body.description += '-beforeUpdateSuffix'; ],
} beforeUpdate: [
return operation; (operation) => {
}, if (operation.req.headers.hook === 'beforeUpdate') {
beforeDelete: (operation) => { operation.req.body.description += '-beforeUpdateSuffix';
if (operation.req.headers.hook === 'beforeDelete') { }
// TODO: Find a better hook operation to assert against in tests return operation.data;
operation.req.headers.hook = 'afterDelete'; },
} ],
return operation; beforeDelete: [
}, (operation) => {
afterCreate: (operation, value) => { if (operation.req.headers.hook === 'beforeDelete') {
if (operation.req.headers.hook === 'afterCreate') { // TODO: Find a better hook operation to assert against in tests
value.afterCreateHook = true; operation.req.headers.hook = 'afterDelete';
} }
return value; },
}, ],
afterRead: (operation) => { afterCreate: [
const { doc } = operation; (operation) => {
doc.afterReadHook = true; if (operation.req.headers.hook === 'afterCreate') {
operation.doc.afterCreateHook = true;
}
return operation.doc;
},
],
afterRead: [
(operation) => {
const { doc } = operation;
doc.afterReadHook = true;
return doc; return doc;
}, },
afterUpdate: (operation, value) => { ],
if (operation.req.headers.hook === 'afterUpdate') { afterUpdate: [
value.afterUpdateHook = true; (operation) => {
} if (operation.req.headers.hook === 'afterUpdate') {
return value; operation.doc.afterUpdateHook = true;
}, }
afterDelete: (operation, value) => { return operation.doc;
if (operation.req.headers.hook === 'afterDelete') { },
value.afterDeleteHook = true; ],
} afterDelete: [
return value; (operation) => {
}, if (operation.req.headers.hook === 'afterDelete') {
operation.doc.afterDeleteHook = true;
}
return operation.doc;
},
],
}, },
fields: [ fields: [
{ {
@@ -73,7 +89,9 @@ module.exports = {
unique: true, unique: true,
localized: true, localized: true,
hooks: { hooks: {
afterRead: value => (value ? value.toUpperCase() : null), afterRead: [
({ value }) => (value ? value.toUpperCase() : null),
],
}, },
}, },
{ {

View File

@@ -4,22 +4,24 @@ module.exports = {
singular: 'Localized Post', singular: 'Localized Post',
plural: 'Localized Posts', plural: 'Localized Posts',
}, },
useAsTitle: 'title', admin: {
policies: { useAsTitle: 'title',
defaultColumns: [
'title',
'priority',
'createdAt',
],
},
access: {
read: () => true, read: () => true,
}, },
preview: (doc, token) => { preview: (doc, token) => {
if (doc.title) { if (doc && doc.title) {
return `http://localhost:3000/posts/${doc.title.value}?preview=true&token=${token}`; return `http://localhost:3000/posts/${doc.title.value}?preview=true&token=${token}`;
} }
return null; return null;
}, },
defaultColumns: [
'title',
'priority',
'createdAt',
],
fields: [ fields: [
{ {
name: 'title', name: 'title',

View File

@@ -0,0 +1,50 @@
const LocalizedArrays = {
slug: 'localized-arrays',
labels: {
singular: 'Localized Array',
plural: 'Localized Arrays',
},
access: {
read: () => true,
},
fields: [
{
type: 'array',
label: 'Array',
name: 'array',
localized: true,
required: true,
minRows: 2,
maxRows: 4,
fields: [
{
type: 'row',
fields: [
{
name: 'arrayText1',
label: 'Array Text 1',
type: 'text',
required: true,
}, {
name: 'arrayText2',
label: 'Array Text 2',
type: 'text',
required: true,
},
],
},
{
type: 'text',
name: 'arrayText3',
label: 'Array Text 3',
admin: {
readOnly: true,
},
},
],
},
],
timestamps: true,
};
module.exports = LocalizedArrays;

View File

@@ -1,48 +0,0 @@
const LocalizedRepeaters = {
slug: 'localized-repeaters',
labels: {
singular: 'Localized Repeater',
plural: 'Localized Repeaters',
},
policies: {
read: () => true,
},
fields: [
{
type: 'repeater',
label: 'Repeater',
name: 'repeater',
localized: true,
required: true,
minRows: 2,
maxRows: 4,
fields: [
{
type: 'row',
fields: [
{
name: 'repeaterText1',
label: 'Repeater Text 1',
type: 'text',
required: true,
}, {
name: 'repeaterText2',
label: 'Repeater Text 2',
type: 'text',
required: true,
},
],
},
{
type: 'text',
name: 'repeaterText3',
label: 'Repeater Text 3',
readOnly: true,
},
],
},
],
timestamps: true,
};
module.exports = LocalizedRepeaters;

View File

@@ -1,4 +1,5 @@
const path = require('path'); const path = require('path');
const checkRole = require('../access/checkRole');
module.exports = { module.exports = {
slug: 'media', slug: 'media',
@@ -6,7 +7,7 @@ module.exports = {
singular: 'Media', singular: 'Media',
plural: 'Media', plural: 'Media',
}, },
policies: { access: {
read: () => true, read: () => true,
}, },
upload: { upload: {
@@ -40,6 +41,25 @@ module.exports = {
type: 'text', type: 'text',
required: true, required: true,
localized: true, localized: true,
hooks: {
afterRead: [
({ value }) => `${value} alt`,
],
},
},
{
name: 'sizes',
fields: [
{
name: 'icon',
access: {
read: ({ req: { user } }) => {
const result = checkRole(['admin'], user);
return result;
},
},
},
],
}, },
], ],
timestamps: true, timestamps: true,

View File

@@ -1,17 +1,17 @@
const NestedRepeater = { const NestedArray = {
slug: 'nested-repeaters', slug: 'nested-arrays',
labels: { labels: {
singular: 'Nested Repeater', singular: 'Nested Array',
plural: 'Nested Repeaters', plural: 'Nested Arrays',
}, },
policies: { access: {
read: () => true, read: () => true,
}, },
fields: [ fields: [
{ {
type: 'repeater', type: 'array',
label: 'Repeater', label: 'Array',
name: 'repeater', name: 'array',
labels: { labels: {
singular: 'Parent Row', singular: 'Parent Row',
plural: 'Parent Rows', plural: 'Parent Rows',
@@ -28,8 +28,8 @@ const NestedRepeater = {
required: true, required: true,
}, },
{ {
type: 'repeater', type: 'array',
name: 'nestedRepeater', name: 'nestedArray',
labels: { labels: {
singular: 'Child Row', singular: 'Child Row',
plural: 'Child Rows', plural: 'Child Rows',
@@ -43,8 +43,8 @@ const NestedRepeater = {
required: true, required: true,
}, },
{ {
type: 'repeater', type: 'array',
name: 'deeplyNestedRepeater', name: 'deeplyNestedArray',
labels: { labels: {
singular: 'Grandchild Row', singular: 'Grandchild Row',
plural: 'Grandchild Rows', plural: 'Grandchild Rows',
@@ -67,4 +67,4 @@ const NestedRepeater = {
timestamps: true, timestamps: true,
}; };
module.exports = NestedRepeater; module.exports = NestedArray;

View File

@@ -4,7 +4,9 @@ module.exports = {
singular: 'Previewable Post', singular: 'Previewable Post',
plural: 'Previewable Posts', plural: 'Previewable Posts',
}, },
useAsTitle: 'title', admin: {
useAsTitle: 'title',
},
preview: (doc, token) => { preview: (doc, token) => {
if (doc.title) { if (doc.title) {
return `http://localhost:3000/previewable-posts/${doc.title.value}?preview=true&token=${token}`; return `http://localhost:3000/previewable-posts/${doc.title.value}?preview=true&token=${token}`;

View File

@@ -1,6 +1,6 @@
const checkRole = require('../policies/checkRole'); const checkRole = require('../access/checkRole');
const policy = ({ req: { user } }) => checkRole(['admin'], user); const access = ({ req: { user } }) => checkRole(['admin'], user);
module.exports = { module.exports = {
slug: 'public-users', slug: 'public-users',
@@ -8,8 +8,10 @@ module.exports = {
singular: 'Public User', singular: 'Public User',
plural: 'Public Users', plural: 'Public Users',
}, },
useAsTitle: 'email', admin: {
policies: { useAsTitle: 'email',
},
access: {
admin: () => false, admin: () => false,
create: () => true, create: () => true,
read: () => true, read: () => true,
@@ -30,6 +32,7 @@ module.exports = {
}, },
auth: { auth: {
tokenExpiration: 300, tokenExpiration: 300,
secureCookie: process.env.NODE_ENV === 'production',
}, },
fields: [ fields: [
{ {
@@ -37,10 +40,10 @@ module.exports = {
label: 'This field should only be readable and editable by Admins with "admin" role', label: 'This field should only be readable and editable by Admins with "admin" role',
type: 'text', type: 'text',
defaultValue: 'test', defaultValue: 'test',
policies: { access: {
create: policy, create: access,
read: policy, read: access,
update: policy, update: access,
}, },
}, },
], ],

View File

@@ -1,6 +1,6 @@
module.exports = { module.exports = {
slug: 'relationship-a', slug: 'relationship-a',
policies: { access: {
read: () => true, read: () => true,
}, },
labels: { labels: {

View File

@@ -1,6 +1,6 @@
module.exports = { module.exports = {
slug: 'relationship-b', slug: 'relationship-b',
policies: { access: {
read: () => true, read: () => true,
}, },
labels: { labels: {

View File

@@ -1,13 +1,15 @@
const checkRole = require('../policies/checkRole'); const checkRole = require('../access/checkRole');
module.exports = { module.exports = {
slug: 'strict-policies', slug: 'strict-access',
labels: { labels: {
singular: 'Strict Policy', singular: 'Strict Access',
plural: 'Strict Policies', plural: 'Strict Access',
}, },
useAsTitle: 'email', admin: {
policies: { useAsTitle: 'email',
},
access: {
create: () => true, create: () => true,
read: ({ req: { user } }) => { read: ({ req: { user } }) => {
if (checkRole(['admin'], user)) { if (checkRole(['admin'], user)) {

View File

@@ -4,7 +4,7 @@ module.exports = {
singular: 'Validation', singular: 'Validation',
plural: 'Validations', plural: 'Validations',
}, },
policies: { access: {
read: () => true, read: () => true,
}, },
fields: [ fields: [
@@ -51,7 +51,7 @@ module.exports = {
], ],
}, },
{ {
type: 'repeater', type: 'array',
label: 'Should have at least 3 rows', label: 'Should have at least 3 rows',
name: 'atLeast3Rows', name: 'atLeast3Rows',
required: true, required: true,
@@ -59,7 +59,7 @@ module.exports = {
const result = value && value.length >= 3; const result = value && value.length >= 3;
if (!result) { if (!result) {
return 'This repeater needs to have at least 3 rows.'; return 'This array needs to have at least 3 rows.';
} }
return true; return true;
@@ -83,9 +83,9 @@ module.exports = {
], ],
}, },
{ {
type: 'repeater', type: 'array',
label: 'Default repeater validation', label: 'Default array validation',
name: 'repeater', name: 'array',
required: true, required: true,
fields: [ fields: [
{ {

View File

@@ -0,0 +1,21 @@
const checkRole = require('../access/checkRole');
const Quote = require('../blocks/Quote');
const CallToAction = require('../blocks/CallToAction');
module.exports = {
slug: 'blocks-global',
label: 'Blocks Global',
access: {
update: ({ req: { user } }) => checkRole(['admin'], user),
read: () => true,
},
fields: [
{
name: 'blocks',
label: 'Blocks',
type: 'blocks',
blocks: [Quote, CallToAction],
localized: true,
},
],
};

View File

@@ -1,21 +0,0 @@
const checkRole = require('../policies/checkRole');
const Quote = require('../content-blocks/Quote');
const CallToAction = require('../content-blocks/CallToAction');
module.exports = {
slug: 'flexible-global',
label: 'Flexible Global',
policies: {
update: ({ req: { user } }) => checkRole(['admin'], user),
read: () => true,
},
fields: [
{
name: 'flexibleGlobal',
label: 'Global Flexible Block',
type: 'flexible',
blocks: [Quote, CallToAction],
localized: true,
},
],
};

View File

@@ -1,20 +0,0 @@
const checkRole = require('../policies/checkRole');
module.exports = {
slug: 'global-with-policies',
label: 'Global with Policies',
policies: {
update: ({ req: { user } }) => checkRole(['admin'], user),
read: ({ req: { user } }) => checkRole(['admin'], user),
},
fields: [
{
name: 'title',
label: 'Site Title',
type: 'text',
localized: true,
maxLength: 100,
required: true,
},
],
};

View File

@@ -0,0 +1,34 @@
const checkRole = require('../access/checkRole');
module.exports = {
slug: 'global-with-access',
label: 'Global with Strict Access',
access: {
update: ({ req: { user } }) => checkRole(['admin'], user),
read: ({ req: { user } }) => checkRole(['admin'], user),
},
fields: [
{
name: 'title',
label: 'Site Title',
type: 'text',
maxLength: 100,
required: true,
},
{
name: 'relationship',
label: 'Test Relationship',
type: 'relationship',
relationTo: 'localized-posts',
hasMany: true,
required: true,
},
{
name: 'singleRelationship',
label: 'Test Single Relationship',
type: 'relationship',
relationTo: 'localized-posts',
required: true,
},
],
};

View File

@@ -1,17 +1,17 @@
const checkRole = require('../policies/checkRole'); const checkRole = require('../access/checkRole');
module.exports = { module.exports = {
slug: 'navigation-repeater', slug: 'navigation-array',
label: 'Navigation Repeater', label: 'Navigation Array',
policies: { access: {
update: ({ req: { user } }) => checkRole(['admin', 'user'], user), update: ({ req: { user } }) => checkRole(['admin', 'user'], user),
read: () => true, read: () => true,
}, },
fields: [ fields: [
{ {
name: 'repeater', name: 'array',
label: 'Repeater', label: 'Array',
type: 'repeater', type: 'array',
localized: true, localized: true,
fields: [{ fields: [{
name: 'text', name: 'text',

View File

@@ -5,13 +5,13 @@ const Code = require('./collections/Code');
const Conditions = require('./collections/Conditions'); const Conditions = require('./collections/Conditions');
const CustomComponents = require('./collections/CustomComponents'); const CustomComponents = require('./collections/CustomComponents');
const File = require('./collections/File'); const File = require('./collections/File');
const FlexibleContent = require('./collections/FlexibleContent'); const Blocks = require('./collections/Blocks');
const HiddenFields = require('./collections/HiddenFields'); const HiddenFields = require('./collections/HiddenFields');
const Hooks = require('./collections/Hooks'); const Hooks = require('./collections/Hooks');
const Localized = require('./collections/Localized'); const Localized = require('./collections/Localized');
const LocalizedRepeaters = require('./collections/LocalizedRepeater'); const LocalizedArray = require('./collections/LocalizedArray');
const Media = require('./collections/Media'); const Media = require('./collections/Media');
const NestedRepeaters = require('./collections/NestedRepeater'); const NestedArrays = require('./collections/NestedArrays');
const Preview = require('./collections/Preview'); const Preview = require('./collections/Preview');
const PublicUsers = require('./collections/PublicUsers'); const PublicUsers = require('./collections/PublicUsers');
const RelationshipA = require('./collections/RelationshipA'); const RelationshipA = require('./collections/RelationshipA');
@@ -20,14 +20,19 @@ const RichText = require('./collections/RichText');
const StrictPolicies = require('./collections/StrictPolicies'); const StrictPolicies = require('./collections/StrictPolicies');
const Validations = require('./collections/Validations'); const Validations = require('./collections/Validations');
const FlexibleGlobal = require('./globals/FlexibleGlobal'); const BlocksGlobal = require('./globals/BlocksGlobal');
const NavigationRepeater = require('./globals/NavigationRepeater'); const NavigationArray = require('./globals/NavigationArray');
const GlobalWithPolicies = require('./globals/GlobalWithPolicies'); const GlobalWithStrictAccess = require('./globals/GlobalWithStrictAccess');
module.exports = { module.exports = {
admin: { admin: {
user: 'admins', user: 'admins',
disable: false, disable: false,
components: {
layout: {
// Sidebar: path.resolve(__dirname, 'client/components/layout/Sidebar/index.js'),
},
},
}, },
collections: [ collections: [
Admin, Admin,
@@ -36,13 +41,13 @@ module.exports = {
Conditions, Conditions,
CustomComponents, CustomComponents,
File, File,
FlexibleContent, Blocks,
HiddenFields, HiddenFields,
Hooks, Hooks,
Localized, Localized,
LocalizedRepeaters, LocalizedArray,
Media, Media,
NestedRepeaters, NestedArrays,
Preview, Preview,
PublicUsers, PublicUsers,
RelationshipA, RelationshipA,
@@ -51,7 +56,11 @@ module.exports = {
StrictPolicies, StrictPolicies,
Validations, Validations,
], ],
globals: [NavigationRepeater, GlobalWithPolicies, FlexibleGlobal], globals: [
NavigationArray,
GlobalWithStrictAccess,
BlocksGlobal,
],
cookiePrefix: 'payload', cookiePrefix: 'payload',
serverURL: 'http://localhost:3000', serverURL: 'http://localhost:3000',
cors: ['http://localhost', 'http://localhost:8080', 'http://localhost:8081'], cors: ['http://localhost', 'http://localhost:8080', 'http://localhost:8081'],
@@ -61,11 +70,11 @@ module.exports = {
graphQL: '/graphql', graphQL: '/graphql',
graphQLPlayground: '/graphql-playground', graphQLPlayground: '/graphql-playground',
}, },
defaultDepth: 2,
compression: {}, compression: {},
paths: { paths: {
scss: path.resolve(__dirname, 'client/scss/overrides.scss'), scss: path.resolve(__dirname, 'client/scss/overrides.scss'),
}, },
mongoURL: 'mongodb://localhost/payload',
graphQL: { graphQL: {
mutations: {}, mutations: {},
queries: {}, queries: {},
@@ -79,17 +88,10 @@ module.exports = {
fallback: true, fallback: true,
}, },
productionGraphQLPlayground: false, productionGraphQLPlayground: false,
components: {
layout: {
// Sidebar: path.resolve(__dirname, 'client/components/layout/Sidebar/index.js'),
},
},
hooks: { hooks: {
afterError: (err, response) => { afterError: () => {
console.error('global error config handler'); console.error('global error config handler');
}, },
}, },
webpack: (config) => { webpack: (config) => config,
return config;
},
}; };

View File

@@ -1,6 +1,8 @@
/* eslint-disable no-console */
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const Payload = require('../src'); const Payload = require('../src');
const logger = require('../src/utilities/logger')();
const expressApp = express(); const expressApp = express();
@@ -13,19 +15,37 @@ const payload = new Payload({
secret: 'SECRET_KEY', secret: 'SECRET_KEY',
mongoURL: 'mongodb://localhost/payload', mongoURL: 'mongodb://localhost/payload',
express: expressApp, express: expressApp,
onInit: () => {
logger.info('Payload is initialized');
// console.log('Payload is initialized');
},
}); });
const externalRouter = express.Router();
externalRouter.use(payload.authenticate());
externalRouter.get('/', (req, res) => {
if (req.user) {
return res.send(`Authenticated successfully as ${req.user.email}.`);
}
return res.send('Not authenticated');
});
expressApp.use('/external-route', externalRouter);
exports.payload = payload; exports.payload = payload;
exports.start = (cb) => { exports.start = (cb) => {
const server = expressApp.listen(3000, async () => { const server = expressApp.listen(3000, async () => {
console.log(`listening on ${3000}...`); logger.info(`listening on ${3000}...`);
if (cb) cb(); if (cb) cb();
const creds = await payload.getMockEmailCredentials(); const creds = await payload.getMockEmailCredentials();
console.log(`Mock email account username: ${creds.user}`); logger.info(`Mock email account username: ${creds.user}`);
console.log(`Mock email account password: ${creds.pass}`); logger.info(`Mock email account password: ${creds.pass}`);
console.log(`Log in to mock email provider at ${creds.web}`); logger.info(`Log in to mock email provider at ${creds.web}`);
}); });
return server; return server;

5
errors.js Normal file
View File

@@ -0,0 +1,5 @@
const { APIError } = require('./src/errors');
module.exports = {
APIError,
};

View File

@@ -1,2 +1,2 @@
export { default as useFieldType } from './src/client/components/forms/useFieldType'; export { default as useFieldType } from './src/client/components/forms/useFieldType';
export { default as useForm } from './src/client/components/forms/Form/useForm'; export { useForm } from './src/client/components/forms/Form/context';

View File

@@ -1,41 +1,44 @@
{ {
"name": "payload", "name": "@payloadcms/payload",
"version": "1.0.0", "version": "0.0.28",
"description": "", "description": "CMS and Application Framework",
"license": "ISC",
"author": "Payload CMS LLC",
"main": "index.js", "main": "index.js",
"scripts": {
"test:unit": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.js NODE_ENV=test jest",
"test:int": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.js NODE_ENV=test jest --forceExit --runInBand",
"cov": "npm run core:build && node ./node_modules/jest/bin/jest.js src/tests --coverage",
"dev": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.js nodemon demo/server.js",
"lint": "eslint **/*.js",
"debug": "nodemon --inspect demo/server.js",
"debug:test:int": "node --inspect-brk node_modules/.bin/jest --runInBand"
},
"bin": { "bin": {
"payload": "./src/bin/index.js" "payload": "./src/bin/index.js"
}, },
"author": "", "scripts": {
"license": "ISC", "cov": "npm run core:build && node ./node_modules/jest/bin/jest.js src/tests --coverage",
"debug": "nodemon --inspect demo/server.js",
"debug:test:int": "node --inspect-brk node_modules/.bin/jest --runInBand",
"dev": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.js nodemon demo/server.js",
"lint": "eslint .",
"test:int": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.js NODE_ENV=test jest --forceExit --runInBand",
"test:unit": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.js NODE_ENV=test jest"
},
"dependencies": { "dependencies": {
"@babel/core": "^7.8.3", "@babel/core": "^7.8.3",
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.8.3", "@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.8.3", "@babel/preset-env": "^7.8.3",
"@babel/preset-react": "^7.8.3", "@babel/preset-react": "^7.8.3",
"@babel/runtime": "^7.8.4", "@babel/runtime": "^7.8.4",
"@date-io/date-fns": "^1.3.13", "@date-io/date-fns": "^1.3.13",
"@trbl/react-collapsibles": "^0.1.0", "@faceless-ui/collapsibles": "^0.1.0",
"@trbl/react-modal": "^1.0.4", "@faceless-ui/modal": "^1.0.4",
"@trbl/react-scroll-info": "^1.1.1", "@faceless-ui/scroll-info": "^1.1.1",
"@trbl/react-window-info": "^1.2.2", "@faceless-ui/window-info": "^1.2.2",
"@udecode/slate-plugins": "^0.60.0", "@udecode/slate-plugins": "^0.60.0",
"async-some": "^1.0.2", "async-some": "^1.0.2",
"autoprefixer": "^9.7.4",
"babel-core": "^7.0.0-bridge.0",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"connect-history-api-fallback": "^1.6.0", "connect-history-api-fallback": "^1.6.0",
"cookie-parser": "^1.4.5", "css-loader": "^1.0.0",
"date-fns": "^2.14.0", "date-fns": "^2.14.0",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"dotenv": "^6.0.0", "dotenv": "^6.0.0",
@@ -44,34 +47,43 @@
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "^1.1.6", "express-fileupload": "^1.1.6",
"express-graphql": "^0.9.0", "express-graphql": "^0.9.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"falsey": "^1.0.0",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",
"flatley": "^5.2.0", "flatley": "^5.2.0",
"graphql": "^15.0.0", "graphql": "^15.0.0",
"graphql-date": "^1.0.3",
"graphql-playground-middleware-express": "^1.7.14", "graphql-playground-middleware-express": "^1.7.14",
"graphql-type-json": "^0.3.1", "graphql-type-json": "^0.3.1",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"http-status": "^1.4.2", "http-status": "^1.4.2",
"image-size": "^0.7.5", "image-size": "^0.7.5",
"is-hotkey": "^0.1.6",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"jest": "^25.3.0", "jest": "^25.3.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"method-override": "^3.0.0", "method-override": "^3.0.0",
"micro-memoize": "^4.0.9",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mongodb-memory-server": "^6.5.2", "mongodb-memory-server": "^6.5.2",
"mongoose": "^5.8.9", "mongoose": "^5.8.9",
"mongoose-autopopulate": "^0.11.0",
"mongoose-hidden": "^1.8.1", "mongoose-hidden": "^1.8.1",
"mongoose-paginate-v2": "^1.3.6", "mongoose-paginate-v2": "^1.3.6",
"node-sass": "^4.13.1",
"node-sass-chokidar": "^1.4.0",
"nodemailer": "^6.4.2", "nodemailer": "^6.4.2",
"object-to-formdata": "^3.0.9", "object-to-formdata": "^3.0.9",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"passport": "^0.4.1", "passport": "^0.4.1",
"passport-anonymous": "^1.0.1", "passport-anonymous": "^1.0.1",
"passport-headerapikey": "^1.2.1", "passport-headerapikey": "^1.2.1",
"passport-jwt": "^4.0.0", "passport-jwt": "^4.0.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"passport-local-mongoose": "^6.0.1", "passport-local-mongoose": "^6.0.1",
"pino": "^6.4.1",
"pino-pretty": "^4.1.0",
"postcss-flexbugs-fixes": "^3.3.1",
"postcss-loader": "^2.1.6",
"postcss-preset-env": "6.0.6", "postcss-preset-env": "6.0.6",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"qs": "^6.9.1", "qs": "^6.9.1",
@@ -83,19 +95,19 @@
"react-document-meta": "^3.0.0-beta.2", "react-document-meta": "^3.0.0-beta.2",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-hook-form": "^5.7.2", "react-hook-form": "^5.7.2",
"react-redux": "^7.2.0",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-router-navigation-prompt": "^1.8.11", "react-router-navigation-prompt": "^1.8.11",
"react-select": "^3.0.8", "react-select": "^3.0.8",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"sass-loader": "7.1.0",
"sharp": "^0.25.2", "sharp": "^0.25.2",
"slate": "^0.58.3", "slate": "^0.58.3",
"slate-history": "^0.58.3", "slate-history": "^0.58.3",
"slate-hyperscript": "^0.58.3", "slate-hyperscript": "^0.58.3",
"slate-react": "^0.58.3", "slate-react": "^0.58.3",
"style-loader": "^0.21.0",
"styled-components": "^5.1.1", "styled-components": "^5.1.1",
"uglifyjs-webpack-plugin": "^1.3.0", "uglifyjs-webpack-plugin": "^2.2.0",
"universal-cookie": "^3.1.0",
"url-loader": "^1.0.1", "url-loader": "^1.0.1",
"uuid": "^8.1.0", "uuid": "^8.1.0",
"val-loader": "^2.1.0", "val-loader": "^2.1.0",
@@ -105,35 +117,19 @@
"webpack-hot-middleware": "^2.25.0" "webpack-hot-middleware": "^2.25.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.8.3", "@trbl/eslint-config": "^1.2.4",
"autoprefixer": "^9.7.4",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"css-loader": "^1.0.0",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-airbnb": "^17.1.0", "eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.20.0", "eslint-plugin-import": "^2.20.0",
"eslint-plugin-jest": "^23.16.0",
"eslint-plugin-jest-dom": "^3.0.1",
"eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-react": "^7.18.0", "eslint-plugin-react": "^7.18.0",
"eslint-plugin-react-hooks": "^2.3.0", "eslint-plugin-react-hooks": "^2.3.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"faker": "^4.1.0", "faker": "^4.1.0",
"node-sass": "^4.13.1", "graphql-request": "^2.0.0",
"node-sass-chokidar": "^1.4.0", "nodemon": "^1.19.4"
"nodemon": "^1.19.4", }
"npm-run-all": "^4.1.5",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-flexbugs-fixes": "^3.3.1",
"postcss-loader": "^2.1.6",
"sass-loader": "7.1.0",
"style-loader": "^0.21.0",
"webpack-cli": "^3.3.11"
},
"browserslist": [
"defaults",
"not IE 11",
"not IE_Mob 11",
"maintained node versions"
]
} }

View File

@@ -72,7 +72,7 @@ describe('Users REST API', () => {
const data = await response.json(); const data = await response.json();
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(data.token).not.toBeNull(); expect(data.refreshedToken).toBeDefined();
token = data.refreshedToken; token = data.refreshedToken;
}); });

View File

@@ -3,11 +3,17 @@ module.exports = [
name: 'enableAPIKey', name: 'enableAPIKey',
type: 'checkbox', type: 'checkbox',
defaultValue: false, defaultValue: false,
admin: {
disabled: true,
},
}, },
{ {
name: 'apiKey', name: 'apiKey',
type: 'text', type: 'text',
minLength: 24, minLength: 24,
maxLength: 48, maxLength: 48,
admin: {
disabled: true,
},
}, },
]; ];

View File

@@ -6,6 +6,9 @@ module.exports = [
label: 'Email', label: 'Email',
type: 'email', type: 'email',
validate: validations.email, validate: validations.email,
admin: {
disabled: true,
},
}, },
{ {
name: 'resetPasswordToken', name: 'resetPasswordToken',

View File

@@ -4,7 +4,9 @@ const defaultUser = {
singular: 'User', singular: 'User',
plural: 'Users', plural: 'Users',
}, },
useAsTitle: 'email', admin: {
useAsTitle: 'email',
},
auth: { auth: {
tokenExpiration: 7200, tokenExpiration: 7200,
}, },

22
src/auth/executeAccess.js Normal file
View File

@@ -0,0 +1,22 @@
const { Forbidden } = require('../errors');
const executeAccess = async (operation, access) => {
if (access) {
const result = await access(operation);
if (!result) {
if (!operation.disableErrors) throw new Forbidden();
}
return result;
}
if (operation.req.user) {
return true;
}
if (!operation.disableErrors) throw new Forbidden();
return false;
};
module.exports = executeAccess;

View File

@@ -1,21 +0,0 @@
const { Forbidden } = require('../errors');
const executePolicy = async (operation, policy) => {
if (policy) {
const result = await policy(operation);
if (!result) {
throw new Forbidden();
}
return result;
}
if (operation.req.user) {
return true;
}
throw new Forbidden();
};
module.exports = executePolicy;

View File

@@ -0,0 +1,40 @@
const executeAccess = require('./executeAccess');
const { Forbidden } = require('../errors');
const getExecuteStaticAccess = ({ config, Model }) => async (req, res, next) => {
try {
if (req.path) {
const accessResult = await executeAccess({ req, isReadingStaticFile: true }, config.access.read);
if (typeof accessResult === 'object') {
const filename = decodeURI(req.path).replace(/^\/|\/$/g, '');
const queryToBuild = {
where: {
and: [
{
filename: {
equals: filename,
},
},
accessResult,
],
},
};
const query = await Model.buildQuery(queryToBuild, req.locale);
const doc = await Model.findOne(query);
if (!doc) {
throw new Forbidden();
}
}
}
return next();
} catch (error) {
return next(error);
}
};
module.exports = getExecuteStaticAccess;

View File

@@ -1,42 +0,0 @@
const executePolicy = require('./executePolicy');
const { Forbidden } = require('../errors');
const getExecuteStaticPolicy = ({ config, Model }) => {
return async (req, res, next) => {
try {
if (req.path) {
const policyResult = await executePolicy({ req, isReadingStaticFile: true }, config.policies.read);
if (typeof policyResult === 'object') {
const filename = decodeURI(req.path).replace(/^\/|\/$/g, '');
const queryToBuild = {
where: {
and: [
{
filename: {
equals: filename,
},
},
policyResult,
],
},
};
const query = await Model.buildQuery(queryToBuild, req.locale);
const doc = await Model.findOne(query);
if (!doc) {
throw new Forbidden();
}
}
}
return next();
} catch (error) {
return next(error);
}
};
};
module.exports = getExecuteStaticPolicy;

21
src/auth/getExtractJWT.js Normal file
View File

@@ -0,0 +1,21 @@
const parseCookies = require('../utilities/parseCookies');
const getExtractJWT = config => (req) => {
const jwtFromHeader = req.get('Authorization');
if (jwtFromHeader && jwtFromHeader.indexOf('JWT ') === 0) {
return jwtFromHeader.replace('JWT ', '');
}
const cookies = parseCookies(req);
const tokenCookieName = `${config.cookiePrefix}-token`;
if (cookies && cookies[tokenCookieName]) {
const token = cookies[tokenCookieName];
return token;
}
return null;
};
module.exports = getExtractJWT;

View File

@@ -1,4 +1,3 @@
const { policies } = require('../../operations');
const formatName = require('../../../graphql/utilities/formatName'); const formatName = require('../../../graphql/utilities/formatName');
const formatConfigNames = (results, configs) => { const formatConfigNames = (results, configs) => {
@@ -13,18 +12,17 @@ const formatConfigNames = (results, configs) => {
return formattedResults; return formattedResults;
}; };
const policyResolver = config => async (_, __, context) => { async function access(_, __, context) {
const options = { const options = {
config, req: context.req,
req: context,
}; };
let policyResults = await policies(options); let accessResults = await this.operations.collections.auth.access(options);
policyResults = formatConfigNames(policyResults, config.collections); accessResults = formatConfigNames(accessResults, this.config.collections);
policyResults = formatConfigNames(policyResults, config.globals); accessResults = formatConfigNames(accessResults, this.config.globals);
return policyResults; return accessResults;
}; }
module.exports = policyResolver; module.exports = access;

View File

@@ -1,18 +1,18 @@
/* eslint-disable no-param-reassign */ function forgotPassword(collection) {
const { forgotPassword } = require('../../operations'); async function resolver(_, args, context) {
const options = {
collection,
data: args,
req: context.req,
};
const forgotPasswordResolver = (config, collection, email) => async (_, args, context) => { await this.operations.collections.auth.forgotPassword(options);
const options = { return true;
config, }
collection,
data: args,
email,
req: context,
};
await forgotPassword(options); const forgotPasswordResolver = resolver.bind(this);
return true; return forgotPasswordResolver;
}; }
module.exports = forgotPasswordResolver; module.exports = forgotPassword;

View File

@@ -1,21 +0,0 @@
const login = require('./login');
const me = require('./me');
const refresh = require('./refresh');
const register = require('./register');
const init = require('./init');
const forgotPassword = require('./forgotPassword');
const resetPassword = require('./resetPassword');
const update = require('./update');
const policies = require('./policies');
module.exports = {
login,
me,
refresh,
init,
register,
forgotPassword,
resetPassword,
update,
policies,
};

View File

@@ -1,15 +1,17 @@
/* eslint-disable no-param-reassign */ function init({ Model }) {
const { init } = require('../../operations'); async function resolver(_, __, context) {
const options = {
Model,
req: context.req,
};
const initResolver = ({ Model }) => async (_, __, context) => { const result = await this.operations.collections.auth.init(options);
const options = {
Model,
req: context,
};
const result = await init(options); return result;
}
return result; const initResolver = resolver.bind(this);
}; return initResolver;
}
module.exports = initResolver; module.exports = init;

View File

@@ -1,20 +1,22 @@
/* eslint-disable no-param-reassign */ function login(collection) {
const { login } = require('../../operations'); async function resolver(_, args, context) {
const options = {
collection,
data: {
email: args.email,
password: args.password,
},
req: context.req,
res: context.res,
};
const loginResolver = (config, collection) => async (_, args, context) => { const token = await this.operations.collections.auth.login(options);
const options = {
collection,
config,
data: {
email: args.email,
password: args.password,
},
req: context,
};
const token = await login(options); return token;
}
return token; const loginResolver = resolver.bind(this);
}; return loginResolver;
}
module.exports = loginResolver; module.exports = login;

View File

@@ -0,0 +1,18 @@
function logout(collection) {
async function resolver(_, __, context) {
const options = {
collection,
res: context.res,
req: context.req,
};
const result = await this.operations.collections.auth.logout(options);
return result;
}
const logoutResolver = resolver.bind(this);
return logoutResolver;
}
module.exports = logout;

View File

@@ -1,6 +1,5 @@
/* eslint-disable no-param-reassign */ async function me(_, __, context) {
const meResolver = async (_, args, context) => { return this.operations.collections.auth.me({ req: context.req });
return context.user; }
};
module.exports = meResolver; module.exports = me;

View File

@@ -1,17 +1,24 @@
/* eslint-disable no-param-reassign */ const getExtractJWT = require('../../getExtractJWT');
const { refresh } = require('../../operations');
const refreshResolver = (config, collection) => async (_, __, context) => { function refresh(collection) {
const options = { async function resolver(_, __, context) {
config, const extractJWT = getExtractJWT(this.config);
collection, const token = extractJWT(context);
authorization: context.headers.authorization,
req: context,
};
const refreshedToken = await refresh(options); const options = {
collection,
token,
req: context.req,
res: context.res,
};
return refreshedToken; const result = await this.operations.collections.auth.refresh(options);
};
module.exports = refreshResolver; return result;
}
const refreshResolver = resolver.bind(this);
return refreshResolver;
}
module.exports = refresh;

View File

@@ -1,28 +1,30 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
const { register } = require('../../operations'); function register(collection) {
async function resolver(_, args, context) {
const options = {
collection,
data: args.data,
depth: 0,
req: context.req,
};
const registerResolver = (config, collection) => async (_, args, context) => { if (args.locale) {
const options = { context.req.locale = args.locale;
config, options.locale = args.locale;
collection, }
data: args.data,
depth: 0,
req: context,
};
if (args.locale) { if (args.fallbackLocale) {
context.locale = args.locale; context.req.fallbackLocale = args.fallbackLocale;
options.locale = args.locale; options.fallbackLocale = args.fallbackLocale;
}
const token = await this.operations.collections.auth.register(options);
return token;
} }
if (args.fallbackLocale) { const registerResolver = resolver.bind(this);
context.fallbackLocale = args.fallbackLocale; return registerResolver;
options.fallbackLocale = args.fallbackLocale; }
}
const token = await register(options); module.exports = register;
return token;
};
module.exports = registerResolver;

View File

@@ -1,22 +1,23 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
const { resetPassword } = require('../../operations'); function resetPassword(collection) {
async function resolver(_, args, context) {
if (args.locale) context.req.locale = args.locale;
if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale;
const resetPasswordResolver = (config, collection) => async (_, args, context) => { const options = {
if (args.locale) context.locale = args.locale; collection,
if (args.fallbackLocale) context.fallbackLocale = args.fallbackLocale; data: args,
req: context.req,
api: 'GraphQL',
};
const options = { const token = await this.operations.collections.auth.resetPassword(options);
collection,
config,
data: args,
req: context,
api: 'GraphQL',
user: context.user,
};
const token = await resetPassword(options); return token;
}
return token; const resetPasswordResolver = resolver.bind(this);
}; return resetPasswordResolver;
}
module.exports = resetPasswordResolver; module.exports = resetPassword;

View File

@@ -1,22 +1,25 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
const { update } = require('../../operations');
const updateResolver = ({ Model, config }) => async (_, args, context) => { function update(collection) {
if (args.locale) context.locale = args.locale; async function resolver(_, args, context) {
if (args.fallbackLocale) context.fallbackLocale = args.fallbackLocale; if (args.locale) context.req.locale = args.locale;
if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale;
const options = { const options = {
config, collection,
Model, data: args.data,
data: args.data, id: args.id,
id: args.id, depth: 0,
depth: 0, req: context.req,
req: context, };
};
const user = await update(options); const user = await this.operations.collections.auth.update(options);
return user; return user;
}; }
module.exports = updateResolver; const updateResolver = resolver.bind(this);
return updateResolver;
}
module.exports = update;

10
src/auth/init.js Normal file
View File

@@ -0,0 +1,10 @@
const passport = require('passport');
const AnonymousStrategy = require('passport-anonymous');
const jwtStrategy = require('./strategies/jwt');
function initAuth() {
passport.use(new AnonymousStrategy.Strategy());
passport.use('jwt', jwtStrategy(this));
}
module.exports = initAuth;

View File

@@ -0,0 +1,107 @@
const allOperations = ['create', 'read', 'update', 'delete'];
async function accessOperation(args) {
const { config } = this;
const {
req,
req: { user },
} = args;
const results = {};
const promises = [];
const isLoggedIn = !!(user);
const userCollectionConfig = (user && user.collection) ? config.collections.find((collection) => collection.slug === user.collection) : null;
const createAccessPromise = async (obj, access, operation, disableWhere = false) => {
const updatedObj = obj;
const result = await access({ req });
if (typeof result === 'object' && !disableWhere) {
updatedObj[operation] = {
permission: true,
where: result,
};
} else {
updatedObj[operation] = {
permission: !!(result),
};
}
};
const executeFieldPolicies = (obj, fields, operation) => {
const updatedObj = obj;
fields.forEach(async (field) => {
if (field.name) {
if (!updatedObj[field.name]) updatedObj[field.name] = {};
if (field.access && typeof field.access[operation] === 'function') {
promises.push(createAccessPromise(updatedObj[field.name], field.access[operation], operation, true));
} else {
updatedObj[field.name][operation] = {
permission: isLoggedIn,
};
}
if (field.type === 'relationship') {
const relatedCollections = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo];
relatedCollections.forEach((slug) => {
const collection = config.collections.find((coll) => coll.slug === slug);
if (collection && collection.access && collection.access[operation]) {
promises.push(createAccessPromise(updatedObj[field.name], collection.access[operation], operation, true));
}
});
}
if (field.fields) {
if (!updatedObj[field.name].fields) updatedObj[field.name].fields = {};
executeFieldPolicies(updatedObj[field.name].fields, field.fields, operation);
}
} else if (field.fields) {
executeFieldPolicies(updatedObj, field.fields, operation);
}
});
};
const executeEntityPolicies = (entity, operations) => {
results[entity.slug] = {
fields: {},
};
operations.forEach((operation) => {
executeFieldPolicies(results[entity.slug].fields, entity.fields, operation);
if (typeof entity.access[operation] === 'function') {
promises.push(createAccessPromise(results[entity.slug], entity.access[operation], operation));
} else {
results[entity.slug][operation] = {
permission: isLoggedIn,
};
}
});
};
if (userCollectionConfig) {
results.canAccessAdmin = userCollectionConfig.access.admin ? userCollectionConfig.access.admin(args) : isLoggedIn;
} else {
results.canAccessAdmin = false;
}
config.collections.forEach((collection) => {
executeEntityPolicies(collection, allOperations);
});
config.globals.forEach((global) => {
executeEntityPolicies(global, ['read', 'update']);
});
await Promise.all(promises);
return results;
}
module.exports = accessOperation;

View File

@@ -1,75 +1,71 @@
const crypto = require('crypto'); const crypto = require('crypto');
const { APIError } = require('../../errors'); const { APIError } = require('../../errors');
const forgotPassword = async (args) => { async function forgotPassword(args) {
try { const { config, sendEmail: email } = this;
if (!Object.prototype.hasOwnProperty.call(args.data, 'email')) {
throw new APIError('Missing email.');
}
let options = { ...args }; if (!Object.prototype.hasOwnProperty.call(args.data, 'email')) {
throw new APIError('Missing email.');
}
// ///////////////////////////////////// let options = { ...args };
// 1. Execute before login hook
// /////////////////////////////////////
const { beforeForgotPassword } = args.collection.config.hooks; // /////////////////////////////////////
// 1. Execute before login hook
// /////////////////////////////////////
if (typeof beforeForgotPassword === 'function') { const { beforeForgotPassword } = args.collection.config.hooks;
options = await beforeForgotPassword(options);
}
// ///////////////////////////////////// if (typeof beforeForgotPassword === 'function') {
// 2. Perform forgot password options = await beforeForgotPassword(options);
// ///////////////////////////////////// }
const { // /////////////////////////////////////
collection: { // 2. Perform forgot password
Model, // /////////////////////////////////////
},
config,
data,
email,
} = options;
let token = await crypto.randomBytes(20); const {
token = token.toString('hex'); collection: {
Model,
},
data,
} = options;
const user = await Model.findOne({ email: data.email }); let token = await crypto.randomBytes(20);
token = token.toString('hex');
if (!user) return; const user = await Model.findOne({ email: data.email });
user.resetPasswordToken = token; if (!user) return;
user.resetPasswordExpiration = Date.now() + 3600000; // 1 hour
await user.save(); user.resetPasswordToken = token;
user.resetPasswordExpiration = Date.now() + 3600000; // 1 hour
const html = `You are receiving this because you (or someone else) have requested the reset of the password for your account. await user.save();
const html = `You are receiving this because you (or someone else) have requested the reset of the password for your account.
Please click on the following link, or paste this into your browser to complete the process: Please click on the following link, or paste this into your browser to complete the process:
<a href="${config.serverURL}${config.routes.admin}/reset/${token}"> <a href="${config.serverURL}${config.routes.admin}/reset/${token}">
${config.serverURL}${config.routes.admin}/reset/${token} ${config.serverURL}${config.routes.admin}/reset/${token}
</a> </a>
If you did not request this, please ignore this email and your password will remain unchanged.`; If you did not request this, please ignore this email and your password will remain unchanged.`;
email({ email({
from: `"${config.email.fromName}" <${config.email.fromAddress}>`, from: `"${config.email.fromName}" <${config.email.fromAddress}>`,
to: data.email, to: data.email,
subject: 'Password Reset', subject: 'Password Reset',
html, html,
}); });
// ///////////////////////////////////// // /////////////////////////////////////
// 3. Execute after forgot password hook // 3. Execute after forgot password hook
// ///////////////////////////////////// // /////////////////////////////////////
const { afterForgotPassword } = args.req.collection.config.hooks; const { afterForgotPassword } = args.req.collection.config.hooks;
if (typeof afterForgotPassword === 'function') { if (typeof afterForgotPassword === 'function') {
await afterForgotPassword(options); await afterForgotPassword(options);
}
} catch (error) {
throw error;
} }
}; }
module.exports = forgotPassword; module.exports = forgotPassword;

View File

@@ -1,21 +0,0 @@
const login = require('./login');
const refresh = require('./refresh');
const register = require('./register');
const init = require('./init');
const forgotPassword = require('./forgotPassword');
const resetPassword = require('./resetPassword');
const registerFirstUser = require('./registerFirstUser');
const update = require('./update');
const policies = require('./policies');
module.exports = {
login,
refresh,
init,
register,
forgotPassword,
update,
resetPassword,
registerFirstUser,
policies,
};

View File

@@ -1,17 +1,13 @@
const init = async (args) => { async function init(args) {
try { const {
const { Model,
Model, } = args;
} = args;
const count = await Model.countDocuments({}); const count = await Model.countDocuments({});
if (count >= 1) return true; if (count >= 1) return true;
return false; return false;
} catch (error) { }
throw error;
}
};
module.exports = init; module.exports = init;

View File

@@ -1,88 +1,112 @@
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { Unauthorized, AuthenticationError } = require('../../errors'); const { AuthenticationError } = require('../../errors');
const login = async (args) => { async function login(args) {
try { const { config, operations } = this;
// Await validation here
let options = { ...args }; const options = { ...args };
// ///////////////////////////////////// // /////////////////////////////////////
// 1. Execute before login hook // 1. Execute before login hook
// ///////////////////////////////////// // /////////////////////////////////////
const beforeLoginHook = args.collection.config.hooks.beforeLogin; args.collection.config.hooks.beforeLogin.forEach((hook) => hook({ req: args.req }));
if (typeof beforeLoginHook === 'function') { // /////////////////////////////////////
options = await beforeLoginHook(options); // 2. Perform login
} // /////////////////////////////////////
// ///////////////////////////////////// const {
// 2. Perform login collection: {
// ///////////////////////////////////// Model,
config: collectionConfig,
},
data,
req,
} = options;
const { const { email, password } = data;
collection: {
Model,
config: collectionConfig,
},
config,
data,
} = options;
const { email, password } = data; const userDoc = await Model.findByUsername(email);
const user = await Model.findByUsername(email);
if (!user) throw new AuthenticationError(); if (!userDoc) throw new AuthenticationError();
const authResult = await user.authenticate(password); const authResult = await userDoc.authenticate(password);
if (!authResult.user) { if (!authResult.user) {
throw new AuthenticationError(); throw new AuthenticationError();
}
const fieldsToSign = collectionConfig.fields.reduce((signedFields, field) => {
if (field.saveToJWT) {
return {
...signedFields,
[field.name]: user[field.name],
};
}
return signedFields;
}, {
email,
id: user.id,
});
fieldsToSign.collection = collectionConfig.slug;
const token = jwt.sign(
fieldsToSign,
config.secret,
{
expiresIn: collectionConfig.auth.tokenExpiration,
},
);
// /////////////////////////////////////
// 3. Execute after login hook
// /////////////////////////////////////
const afterLoginHook = args.collection.config.hooks.afterLogin;
if (typeof afterLoginHook === 'function') {
await afterLoginHook({ ...options, token, user });
}
// /////////////////////////////////////
// 4. Return token
// /////////////////////////////////////
return token;
} catch (error) {
throw error;
} }
};
const userQuery = await operations.collections.find({
where: {
email: {
equals: email,
},
},
collection: {
Model,
config: collectionConfig,
},
req,
overrideAccess: true,
});
const user = userQuery.docs[0];
const fieldsToSign = collectionConfig.fields.reduce((signedFields, field) => {
if (field.saveToJWT) {
return {
...signedFields,
[field.name]: user[field.name],
};
}
return signedFields;
}, {
email,
id: user.id,
});
fieldsToSign.collection = collectionConfig.slug;
const token = jwt.sign(
fieldsToSign,
config.secret,
{
expiresIn: collectionConfig.auth.tokenExpiration,
},
);
if (args.res) {
const cookieOptions = {
path: '/',
httpOnly: true,
};
if (collectionConfig.auth.secureCookie) {
cookieOptions.secure = true;
}
if (args.req.headers.origin && args.req.headers.origin.indexOf('localhost') === -1) {
let domain = args.req.headers.origin.replace('https://', '');
domain = args.req.headers.origin.replace('http://', '');
cookieOptions.domain = domain;
}
args.res.cookie(`${config.cookiePrefix}-token`, token, cookieOptions);
}
// /////////////////////////////////////
// 3. Execute after login hook
// /////////////////////////////////////
args.collection.config.hooks.afterLogin.forEach((hook) => hook({ token, user, req: args.req }));
// /////////////////////////////////////
// 4. Return token
// /////////////////////////////////////
return token;
}
module.exports = login; module.exports = login;

View File

@@ -0,0 +1,34 @@
async function logout(args) {
const { config } = this;
const {
collection: {
config: collectionConfig,
},
res,
req,
} = args;
const cookieOptions = {
expires: new Date(0),
httpOnly: true,
path: '/',
overwrite: true,
};
if (collectionConfig.auth && collectionConfig.auth.secureCookie) {
cookieOptions.secure = true;
}
if (req.headers.origin && req.headers.origin.indexOf('localhost') === -1) {
let domain = req.headers.origin.replace('https://', '');
domain = req.headers.origin.replace('http://', '');
cookieOptions.domain = domain;
}
res.cookie(`${config.cookiePrefix}-token`, '', cookieOptions);
return 'Logged out successfully.';
}
module.exports = logout;

31
src/auth/operations/me.js Normal file
View File

@@ -0,0 +1,31 @@
const jwt = require('jsonwebtoken');
const getExtractJWT = require('../getExtractJWT');
async function me({ req }) {
const extractJWT = getExtractJWT(this.config);
if (req.user) {
const response = {
user: req.user,
};
const token = extractJWT(req);
if (token) {
response.token = token;
const decoded = jwt.decode(token);
if (decoded) {
response.user.exp = decoded.exp;
}
}
return response;
}
return {
user: null,
};
}
module.exports = me;

View File

@@ -1,98 +0,0 @@
const allOperations = ['create', 'read', 'update', 'delete'];
const policies = async (args) => {
const {
config,
req,
req: { user },
} = args;
const results = {};
const promises = [];
const isLoggedIn = !!(user);
const userCollectionConfig = (user && user.collection) ? config.collections.find(collection => collection.slug === user.collection) : null;
const createPolicyPromise = async (obj, policy, operation, disableWhere = false) => {
const updatedObj = obj;
const result = await policy({ req });
if (typeof result === 'object' && !disableWhere) {
updatedObj[operation] = {
permission: true,
where: result,
};
} else {
updatedObj[operation] = {
permission: !!(result),
};
}
};
const executeFieldPolicies = (obj, fields, operation) => {
const updatedObj = obj;
fields.forEach((field) => {
if (field.name) {
if (!updatedObj[field.name]) updatedObj[field.name] = {};
if (field.policies && typeof field.policies[operation] === 'function') {
promises.push(createPolicyPromise(updatedObj[field.name], field.policies[operation], operation, true));
} else {
updatedObj[field.name][operation] = {
permission: isLoggedIn,
};
}
if (field.fields) {
if (!updatedObj[field.name].fields) updatedObj[field.name].fields = {};
executeFieldPolicies(updatedObj[field.name].fields, field.fields, operation);
}
} else if (field.fields) {
executeFieldPolicies(updatedObj, field.fields, operation);
}
});
};
const executeEntityPolicies = (entity, operations) => {
results[entity.slug] = {
fields: {},
};
operations.forEach((operation) => {
executeFieldPolicies(results[entity.slug].fields, entity.fields, operation);
if (typeof entity.policies[operation] === 'function') {
promises.push(createPolicyPromise(results[entity.slug], entity.policies[operation], operation));
} else {
results[entity.slug][operation] = {
permission: isLoggedIn,
};
}
});
};
try {
if (userCollectionConfig) {
results.canAccessAdmin = userCollectionConfig.policies.admin ? userCollectionConfig.policies.admin(args) : isLoggedIn;
} else {
results.canAccessAdmin = false;
}
config.collections.forEach((collection) => {
executeEntityPolicies(collection, allOperations);
});
config.globals.forEach((global) => {
executeEntityPolicies(global, ['read', 'update']);
});
await Promise.all(promises);
return results;
} catch (error) {
throw error;
}
};
module.exports = policies;

View File

@@ -1,53 +1,76 @@
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { Forbidden } = require('../../errors');
const refresh = async (args) => { async function refresh(args) {
try { // Await validation here
// Await validation here
let options = { ...args }; const { secret, cookiePrefix } = this.config;
let options = { ...args };
// ///////////////////////////////////// // /////////////////////////////////////
// 1. Execute before refresh hook // 1. Execute before refresh hook
// ///////////////////////////////////// // /////////////////////////////////////
const { beforeRefresh } = args.collection.config.hooks; const { beforeRefresh } = args.collection.config.hooks;
if (typeof beforeRefresh === 'function') { if (typeof beforeRefresh === 'function') {
options = await beforeRefresh(options); options = await beforeRefresh(options);
}
// /////////////////////////////////////
// 2. Perform refresh
// /////////////////////////////////////
const { secret } = options.config;
const opts = {};
opts.expiresIn = options.collection.config.auth.tokenExpiration;
const token = options.authorization.replace('JWT ', '');
const payload = jwt.verify(token, secret, {});
delete payload.iat;
delete payload.exp;
const refreshedToken = jwt.sign(payload, secret, opts);
// /////////////////////////////////////
// 3. Execute after login hook
// /////////////////////////////////////
const { afterRefresh } = args.collection.config.hooks;
if (typeof afterRefresh === 'function') {
await afterRefresh(options, refreshedToken);
}
// /////////////////////////////////////
// 4. Return refreshed token
// /////////////////////////////////////
return refreshedToken;
} catch (error) {
throw error;
} }
};
// /////////////////////////////////////
// 2. Perform refresh
// /////////////////////////////////////
const opts = {};
opts.expiresIn = options.collection.config.auth.tokenExpiration;
if (typeof options.token !== 'string') throw new Forbidden();
const payload = jwt.verify(options.token, secret, {});
delete payload.iat;
delete payload.exp;
const refreshedToken = jwt.sign(payload, secret, opts);
if (args.res) {
const cookieOptions = {
path: '/',
httpOnly: true,
};
if (options.collection.config.auth.secureCookie) {
cookieOptions.secure = true;
}
if (args.req.headers.origin && args.req.headers.origin.indexOf('localhost') === -1) {
let domain = args.req.headers.origin.replace('https://', '');
domain = args.req.headers.origin.replace('http://', '');
cookieOptions.domain = domain;
}
args.res.cookie(`${cookiePrefix}-token`, refreshedToken, cookieOptions);
}
// /////////////////////////////////////
// 3. Execute after login hook
// /////////////////////////////////////
const { afterRefresh } = args.collection.config.hooks;
if (typeof afterRefresh === 'function') {
await afterRefresh(options, refreshedToken);
}
// /////////////////////////////////////
// 4. Return refreshed token
// /////////////////////////////////////
payload.exp = jwt.decode(refreshedToken).exp;
return {
refreshedToken,
user: payload,
};
}
module.exports = refresh; module.exports = refresh;

View File

@@ -1,93 +1,106 @@
const passport = require('passport'); const passport = require('passport');
const executePolicy = require('../executePolicy'); const executeAccess = require('../executeAccess');
const performFieldOperations = require('../../fields/performFieldOperations');
const register = async (args) => { async function register(args) {
try { const {
// ///////////////////////////////////// depth,
// 1. Retrieve and execute policy overrideAccess,
// ///////////////////////////////////// collection: {
Model,
config: collectionConfig,
},
req,
req: {
locale,
fallbackLocale,
},
} = args;
if (!args.overridePolicy) { let { data } = args;
await executePolicy(args, args.collection.config.policies.create);
}
let options = { ...args }; // /////////////////////////////////////
// 1. Retrieve and execute access
// /////////////////////////////////////
// ///////////////////////////////////// if (!overrideAccess) {
// 2. Execute before register hook await executeAccess({ req }, collectionConfig.access.create);
// /////////////////////////////////////
const { beforeRegister } = args.collection.config.hooks;
if (typeof beforeRegister === 'function') {
options = await beforeRegister(options);
}
// /////////////////////////////////////
// 3. Execute field-level hooks, policies, and validation
// /////////////////////////////////////
options.data = await performFieldOperations(args.collection.config, { ...options, hook: 'beforeCreate', operationName: 'create' });
// /////////////////////////////////////
// 6. Perform register
// /////////////////////////////////////
const {
collection: {
Model,
},
data,
req: {
locale,
fallbackLocale,
},
} = options;
const modelData = { ...data };
delete modelData.password;
const user = new Model();
if (locale && user.setLocale) {
user.setLocale(locale, fallbackLocale);
}
Object.assign(user, modelData);
let result = await Model.register(user, data.password);
await passport.authenticate('local');
result = result.toJSON({ virtuals: true });
// /////////////////////////////////////
// 7. Execute field-level hooks and policies
// /////////////////////////////////////
result = await performFieldOperations(args.collection.config, {
...options, data: result, hook: 'afterRead', operationName: 'read',
});
// /////////////////////////////////////
// 8. Execute after register hook
// /////////////////////////////////////
const afterRegister = args.collection.config.hooks;
if (typeof afterRegister === 'function') {
result = await afterRegister(options, result);
}
// /////////////////////////////////////
// 9. Return user
// /////////////////////////////////////
return result;
} catch (error) {
throw error;
} }
};
// /////////////////////////////////////
// 2. Execute before create hook
// /////////////////////////////////////
await collectionConfig.hooks.beforeCreate.reduce(async (priorHook, hook) => {
await priorHook;
data = (await hook({
data,
req,
})) || data;
}, Promise.resolve());
// /////////////////////////////////////
// 3. Execute field-level hooks, access, and validation
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data,
hook: 'beforeCreate',
operationName: 'create',
req,
});
// /////////////////////////////////////
// 6. Perform register
// /////////////////////////////////////
const modelData = { ...data };
delete modelData.password;
const user = new Model();
if (locale && user.setLocale) {
user.setLocale(locale, fallbackLocale);
}
Object.assign(user, modelData);
let result = await Model.register(user, data.password);
await passport.authenticate('local');
result = result.toJSON({ virtuals: true });
// /////////////////////////////////////
// 7. Execute field-level hooks and access
// /////////////////////////////////////
result = await this.performFieldOperations(collectionConfig, {
data: result,
hook: 'afterRead',
operationName: 'read',
req,
depth,
});
// /////////////////////////////////////
// 8. Execute after create hook
// /////////////////////////////////////
await collectionConfig.hooks.afterCreate.reduce(async (priorHook, hook) => {
await priorHook;
result = await hook({
doc: result,
req: args.req,
}) || result;
}, Promise.resolve());
// /////////////////////////////////////
// 9. Return user
// /////////////////////////////////////
return result;
}
module.exports = register; module.exports = register;

View File

@@ -1,64 +1,43 @@
const register = require('./register');
const login = require('./login');
const { Forbidden } = require('../../errors'); const { Forbidden } = require('../../errors');
const registerFirstUser = async (args) => { async function registerFirstUser(args) {
try { const {
const count = await args.collection.Model.countDocuments({}); collection: {
Model,
},
} = args;
if (count >= 1) throw new Forbidden(); const count = await Model.countDocuments({});
// Await validation here if (count >= 1) throw new Forbidden();
let options = { ...args }; // /////////////////////////////////////
// 2. Perform register first user
// /////////////////////////////////////
// ///////////////////////////////////// let result = await this.operations.collections.auth.register({
// 1. Execute before register first user hook ...args,
// ///////////////////////////////////// overrideAccess: true,
});
const { beforeRegister } = args.collection.config.hooks;
if (typeof beforeRegister === 'function') {
options = await beforeRegister(options);
}
// /////////////////////////////////////
// 2. Perform register first user
// /////////////////////////////////////
let result = await register({
...options,
overridePolicy: true,
});
// ///////////////////////////////////// // /////////////////////////////////////
// 3. Log in new user // 3. Log in new user
// ///////////////////////////////////// // /////////////////////////////////////
const token = await login({ const token = await this.operations.collections.auth.login({
...options, ...args,
}); });
result = { result = {
...result, ...result,
token, token,
}; };
// ///////////////////////////////////// return {
// 4. Execute after register first user hook message: 'Registered successfully. Welcome to Payload!',
// ///////////////////////////////////// user: result,
};
const afterRegister = args.config.hooks; }
if (typeof afterRegister === 'function') {
result = await afterRegister(options, result);
}
return result;
} catch (error) {
throw error;
}
};
module.exports = registerFirstUser; module.exports = registerFirstUser;

View File

@@ -1,94 +1,91 @@
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { APIError } = require('../../errors'); const { APIError } = require('../../errors');
const resetPassword = async (args) => { async function resetPassword(args) {
try { const { config } = this;
if (!Object.prototype.hasOwnProperty.call(args.data, 'token')
|| !Object.prototype.hasOwnProperty.call(args.data, 'password')) {
throw new APIError('Missing required data.');
}
let options = { ...args }; if (!Object.prototype.hasOwnProperty.call(args.data, 'token')
|| !Object.prototype.hasOwnProperty.call(args.data, 'password')) {
// ///////////////////////////////////// throw new APIError('Missing required data.');
// 1. Execute before reset password hook
// /////////////////////////////////////
const { beforeResetPassword } = args.collection.config.hooks;
if (typeof beforeResetPassword === 'function') {
options = await beforeResetPassword(options);
}
// /////////////////////////////////////
// 2. Perform password reset
// /////////////////////////////////////
const {
collection: {
Model,
config: collectionConfig,
},
config,
data,
} = options;
const { email } = data;
const user = await Model.findOne({
resetPasswordToken: data.token,
resetPasswordExpiration: { $gt: Date.now() },
});
if (!user) throw new APIError('Token is either invalid or has expired.');
await user.setPassword(data.password);
user.resetPasswordExpiration = Date.now();
await user.save();
await user.authenticate(data.password);
const fieldsToSign = collectionConfig.fields.reduce((signedFields, field) => {
if (field.saveToJWT) {
return {
...signedFields,
[field.name]: user[field.name],
};
}
return signedFields;
}, {
email,
});
const token = jwt.sign(
fieldsToSign,
config.secret,
{
expiresIn: collectionConfig.auth.tokenExpiration,
},
);
// /////////////////////////////////////
// 3. Execute after reset password hook
// /////////////////////////////////////
const { afterResetPassword } = collectionConfig.hooks;
if (typeof afterResetPassword === 'function') {
await afterResetPassword(options, user);
}
// /////////////////////////////////////
// 4. Return updated user
// /////////////////////////////////////
return token;
} catch (error) {
throw error;
} }
};
let options = { ...args };
// /////////////////////////////////////
// 1. Execute before reset password hook
// /////////////////////////////////////
const { beforeResetPassword } = args.collection.config.hooks;
if (typeof beforeResetPassword === 'function') {
options = await beforeResetPassword(options);
}
// /////////////////////////////////////
// 2. Perform password reset
// /////////////////////////////////////
const {
collection: {
Model,
config: collectionConfig,
},
data,
} = options;
const { email } = data;
const user = await Model.findOne({
resetPasswordToken: data.token,
resetPasswordExpiration: { $gt: Date.now() },
});
if (!user) throw new APIError('Token is either invalid or has expired.');
await user.setPassword(data.password);
user.resetPasswordExpiration = Date.now();
await user.save();
await user.authenticate(data.password);
const fieldsToSign = collectionConfig.fields.reduce((signedFields, field) => {
if (field.saveToJWT) {
return {
...signedFields,
[field.name]: user[field.name],
};
}
return signedFields;
}, {
email,
});
const token = jwt.sign(
fieldsToSign,
config.secret,
{
expiresIn: collectionConfig.auth.tokenExpiration,
},
);
// /////////////////////////////////////
// 3. Execute after reset password hook
// /////////////////////////////////////
const { afterResetPassword } = collectionConfig.hooks;
if (typeof afterResetPassword === 'function') {
await afterResetPassword(options, user);
}
// /////////////////////////////////////
// 4. Return updated user
// /////////////////////////////////////
return token;
}
module.exports = resetPassword; module.exports = resetPassword;

View File

@@ -1,123 +1,142 @@
const deepmerge = require('deepmerge'); const deepmerge = require('deepmerge');
const overwriteMerge = require('../../utilities/overwriteMerge'); const overwriteMerge = require('../../utilities/overwriteMerge');
const { NotFound, Forbidden } = require('../../errors'); const { NotFound, Forbidden } = require('../../errors');
const executePolicy = require('../executePolicy'); const executeAccess = require('../executeAccess');
const performFieldOperations = require('../../fields/performFieldOperations');
const update = async (args) => { async function update(args) {
try { const { config } = this;
// /////////////////////////////////////
// 1. Execute policy
// /////////////////////////////////////
const policyResults = await executePolicy(args, args.config.policies.update); const {
const hasWherePolicy = typeof policyResults === 'object'; depth,
collection: {
let options = { ...args };
// /////////////////////////////////////
// 2. Retrieve document
// /////////////////////////////////////
const {
Model, Model,
id, config: collectionConfig,
req: { },
locale, id,
fallbackLocale, req,
}, req: {
} = options; locale,
fallbackLocale,
},
} = args;
let query = { _id: id }; // /////////////////////////////////////
// 1. Execute access
// /////////////////////////////////////
if (hasWherePolicy) { const accessResults = await executeAccess({ req }, collectionConfig.access.update);
query = { const hasWhereAccess = typeof accessResults === 'object';
...query,
...policyResults,
};
}
let user = await Model.findOne(query); // /////////////////////////////////////
// 2. Retrieve document
// /////////////////////////////////////
if (!user && !hasWherePolicy) throw new NotFound(); let query = { _id: id };
if (!user && hasWherePolicy) throw new Forbidden();
if (locale && user.setLocale) { if (hasWhereAccess) {
user.setLocale(locale, fallbackLocale); query = {
} ...query,
...accessResults,
const userJSON = user.toJSON({ virtuals: true }); };
// /////////////////////////////////////
// 2. Execute before update hook
// /////////////////////////////////////
const { beforeUpdate } = args.config.hooks;
if (typeof beforeUpdate === 'function') {
options = await beforeUpdate(options);
}
// /////////////////////////////////////
// 3. Merge updates into existing data
// /////////////////////////////////////
options.data = deepmerge(userJSON, options.data, { arrayMerge: overwriteMerge });
// /////////////////////////////////////
// 4. Execute field-level hooks, policies, and validation
// /////////////////////////////////////
options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeUpdate', operationName: 'update' });
// /////////////////////////////////////
// 5. Handle password update
// /////////////////////////////////////
const dataToUpdate = { ...options.data };
const { password } = dataToUpdate;
if (password) {
delete dataToUpdate.password;
await user.setPassword(password);
}
// /////////////////////////////////////
// 6. Perform database operation
// /////////////////////////////////////
Object.assign(user, dataToUpdate);
await user.save();
user = user.toJSON({ virtuals: true });
// /////////////////////////////////////
// 7. Execute field-level hooks and policies
// /////////////////////////////////////
user = performFieldOperations(args.config, {
...options, data: user, hook: 'afterRead', operationName: 'read',
});
// /////////////////////////////////////
// 8. Execute after update hook
// /////////////////////////////////////
const afterUpdateHook = args.config.hooks && args.config.hooks.afterUpdate;
if (typeof afterUpdateHook === 'function') {
user = await afterUpdateHook(options, user);
}
// /////////////////////////////////////
// 9. Return user
// /////////////////////////////////////
return user;
} catch (error) {
throw error;
} }
};
let user = await Model.findOne(query);
if (!user && !hasWhereAccess) throw new NotFound();
if (!user && hasWhereAccess) throw new Forbidden();
if (locale && user.setLocale) {
user.setLocale(locale, fallbackLocale);
}
const originalDoc = user.toJSON({ virtuals: true });
let { data } = args;
// /////////////////////////////////////
// 2. Execute before update hook
// /////////////////////////////////////
await collectionConfig.hooks.beforeUpdate.reduce(async (priorHook, hook) => {
await priorHook;
data = (await hook({
data,
req,
originalDoc,
})) || data;
}, Promise.resolve());
// /////////////////////////////////////
// 3. Merge updates into existing data
// /////////////////////////////////////
data = deepmerge(originalDoc, data, { arrayMerge: overwriteMerge });
// /////////////////////////////////////
// 4. Execute field-level hooks, access, and validation
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data,
req,
hook: 'beforeUpdate',
operationName: 'update',
originalDoc,
});
// /////////////////////////////////////
// 5. Handle password update
// /////////////////////////////////////
const dataToUpdate = { ...data };
const { password } = dataToUpdate;
if (password) {
delete dataToUpdate.password;
await user.setPassword(password);
}
// /////////////////////////////////////
// 6. Perform database operation
// /////////////////////////////////////
Object.assign(user, dataToUpdate);
await user.save();
user = user.toJSON({ virtuals: true });
// /////////////////////////////////////
// 7. Execute field-level hooks and access
// /////////////////////////////////////
user = this.performFieldOperations(collectionConfig, {
data: user,
hook: 'afterRead',
operationName: 'read',
req,
depth,
});
// /////////////////////////////////////
// 8. Execute after update hook
// /////////////////////////////////////
await collectionConfig.hooks.afterUpdate.reduce(async (priorHook, hook) => {
await priorHook;
user = await hook({
doc: user,
req,
}) || user;
}, Promise.resolve());
// /////////////////////////////////////
// 9. Return user
// /////////////////////////////////////
return user;
}
module.exports = update; module.exports = update;

View File

@@ -0,0 +1,16 @@
const httpStatus = require('http-status');
async function policiesHandler(req, res, next) {
try {
const accessResults = await this.operations.collections.auth.access({
req,
});
return res.status(httpStatus.OK)
.json(accessResults);
} catch (error) {
return next(error);
}
}
module.exports = policiesHandler;

View File

@@ -1,15 +1,11 @@
const httpStatus = require('http-status'); const httpStatus = require('http-status');
const formatErrorResponse = require('../../express/responses/formatError');
const { forgotPassword } = require('../operations');
const forgotPasswordHandler = (config, email) => async (req, res) => { async function forgotPasswordHandler(req, res, next) {
try { try {
await forgotPassword({ await this.operations.collections.auth.forgotPassword({
req, req,
collection: req.collection, collection: req.collection,
config,
data: req.body, data: req.body,
email,
}); });
return res.status(httpStatus.OK) return res.status(httpStatus.OK)
@@ -17,8 +13,8 @@ const forgotPasswordHandler = (config, email) => async (req, res) => {
message: 'Success', message: 'Success',
}); });
} catch (error) { } catch (error) {
return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); return next(error);
} }
}; }
module.exports = forgotPasswordHandler; module.exports = forgotPasswordHandler;

View File

@@ -1,23 +0,0 @@
const login = require('./login');
const me = require('./me');
const refresh = require('./refresh');
const register = require('./register');
const init = require('./init');
const forgotPassword = require('./forgotPassword');
const resetPassword = require('./resetPassword');
const registerFirstUser = require('./registerFirstUser');
const update = require('./update');
const policies = require('./policies');
module.exports = {
login,
me,
refresh,
init,
register,
forgotPassword,
registerFirstUser,
resetPassword,
update,
policies,
};

View File

@@ -1,14 +1,10 @@
const httpStatus = require('http-status'); async function initHandler(req, res, next) {
const { init } = require('../operations');
const formatError = require('../../express/responses/formatError');
const initHandler = async (req, res) => {
try { try {
const initialized = await init({ Model: req.collection.Model }); const initialized = await this.operations.collections.auth.init({ Model: req.collection.Model });
return res.status(200).json({ initialized }); return res.status(200).json({ initialized });
} catch (error) { } catch (error) {
return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatError(error)); return next(error);
} }
}; }
module.exports = initHandler; module.exports = initHandler;

View File

@@ -1,13 +1,11 @@
const httpStatus = require('http-status'); const httpStatus = require('http-status');
const formatErrorResponse = require('../../express/responses/formatError');
const { login } = require('../operations');
const loginHandler = config => async (req, res) => { async function loginHandler(req, res, next) {
try { try {
const token = await login({ const token = await this.operations.collections.auth.login({
req, req,
res,
collection: req.collection, collection: req.collection,
config,
data: req.body, data: req.body,
}); });
@@ -17,8 +15,8 @@ const loginHandler = config => async (req, res) => {
token, token,
}); });
} catch (error) { } catch (error) {
return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); return next(error);
} }
}; }
module.exports = loginHandler; module.exports = loginHandler;

View File

@@ -0,0 +1,15 @@
async function logoutHandler(req, res, next) {
try {
const message = await this.operations.collections.auth.logout({
collection: req.collection,
res,
req,
});
return res.status(200).json({ message });
} catch (error) {
return next(error);
}
}
module.exports = logoutHandler;

View File

@@ -1,5 +1,10 @@
const meHandler = async (req, res) => { async function me(req, res, next) {
return res.status(200).json(req.user); try {
}; const response = await this.operations.collections.auth.me({ req });
return res.status(200).json(response);
} catch (err) {
return next(err);
}
}
module.exports = meHandler; module.exports = me;

View File

@@ -1,19 +0,0 @@
const httpStatus = require('http-status');
const formatErrorResponse = require('../../express/responses/formatError');
const { policies } = require('../operations');
const policiesHandler = config => async (req, res) => {
try {
const policyResults = await policies({
req,
config,
});
return res.status(httpStatus.OK)
.json(policyResults);
} catch (error) {
return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error));
}
};
module.exports = policiesHandler;

View File

@@ -1,23 +1,24 @@
const httpStatus = require('http-status'); const getExtractJWT = require('../getExtractJWT');
const formatErrorResponse = require('../../express/responses/formatError');
const { refresh } = require('../operations');
const refreshHandler = config => async (req, res) => { async function refreshHandler(req, res, next) {
try { try {
const refreshedToken = await refresh({ const extractJWT = getExtractJWT(this.config);
const token = extractJWT(req);
const result = await this.operations.collections.auth.refresh({
req, req,
res,
collection: req.collection, collection: req.collection,
config, token,
authorization: req.headers.authorization,
}); });
return res.status(200).json({ return res.status(200).json({
message: 'Token refresh successful', message: 'Token refresh successful',
refreshedToken, ...result,
}); });
} catch (error) { } catch (error) {
return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); return next(error);
} }
}; }
module.exports = refreshHandler; module.exports = refreshHandler;

View File

@@ -1,12 +1,9 @@
const httpStatus = require('http-status'); const httpStatus = require('http-status');
const formatErrorResponse = require('../../express/responses/formatError');
const formatSuccessResponse = require('../../express/responses/formatSuccess'); const formatSuccessResponse = require('../../express/responses/formatSuccess');
const { register } = require('../operations');
const registerHandler = config => async (req, res) => { async function register(req, res, next) {
try { try {
const user = await register({ const user = await this.operations.collections.auth.register({
config,
collection: req.collection, collection: req.collection,
req, req,
data: req.body, data: req.body,
@@ -17,8 +14,8 @@ const registerHandler = config => async (req, res) => {
doc: user, doc: user,
}); });
} catch (error) { } catch (error) {
return res.status(error.status || httpStatus.UNAUTHORIZED).json(formatErrorResponse(error)); return next(error);
} }
}; }
module.exports = registerHandler; module.exports = register;

View File

@@ -1,20 +1,16 @@
const httpStatus = require('http-status'); async function registerFirstUser(req, res, next) {
const formatErrorResponse = require('../../express/responses/formatError');
const { registerFirstUser } = require('../operations');
const registerFirstUserHandler = config => async (req, res) => {
try { try {
const firstUser = await registerFirstUser({ const firstUser = await this.operations.collections.auth.registerFirstUser({
req, req,
config, res,
collection: req.collection, collection: req.collection,
data: req.body, data: req.body,
}); });
return res.status(201).json(firstUser); return res.status(201).json(firstUser);
} catch (error) { } catch (error) {
return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); return next(error);
} }
}; }
module.exports = registerFirstUserHandler; module.exports = registerFirstUser;

View File

@@ -1,13 +1,10 @@
const httpStatus = require('http-status'); const httpStatus = require('http-status');
const formatErrorResponse = require('../../express/responses/formatError');
const { resetPassword } = require('../operations');
const resetPasswordHandler = config => async (req, res) => { async function resetPassword(req, res, next) {
try { try {
const token = await resetPassword({ const token = await this.operations.collections.auth.resetPassword({
req, req,
collection: req.collection, collection: req.collection,
config,
data: req.body, data: req.body,
}); });
@@ -17,9 +14,8 @@ const resetPasswordHandler = config => async (req, res) => {
token, token,
}); });
} catch (error) { } catch (error) {
return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR) return next(error);
.json(formatErrorResponse(error));
} }
}; }
module.exports = resetPasswordHandler; module.exports = resetPassword;

View File

@@ -1,15 +1,12 @@
const httpStatus = require('http-status'); const httpStatus = require('http-status');
const formatErrorResponse = require('../../express/responses/formatError');
const formatSuccessResponse = require('../../express/responses/formatSuccess'); const formatSuccessResponse = require('../../express/responses/formatSuccess');
const { update } = require('../operations');
const updateHandler = async (req, res) => { async function update(req, res, next) {
try { try {
const user = await update({ const user = await this.operations.collections.auth.update({
req, req,
data: req.body, data: req.body,
Model: req.collection.Model, collection: req.collection,
config: req.collection.config,
id: req.params.id, id: req.params.id,
}); });
@@ -18,8 +15,8 @@ const updateHandler = async (req, res) => {
doc: user, doc: user,
}); });
} catch (error) { } catch (error) {
return res.status(httpStatus.UNAUTHORIZED).json(formatErrorResponse(error)); return next(error);
} }
}; }
module.exports = updateHandler; module.exports = update;

View File

@@ -1,74 +0,0 @@
const express = require('express');
const bindCollectionMiddleware = require('../collections/bindCollection');
const {
init,
login,
refresh,
me,
register,
registerFirstUser,
forgotPassword,
resetPassword,
update,
} = require('./requestHandlers');
const {
find,
findByID,
deleteHandler,
} = require('../collections/requestHandlers');
const router = express.Router();
const authRoutes = (collection, config, sendEmail) => {
const { slug } = collection.config;
router.all('*',
bindCollectionMiddleware(collection));
router
.route(`/${slug}/init`)
.get(init);
router
.route(`/${slug}/login`)
.post(login(config));
router
.route(`/${slug}/refresh-token`)
.post(refresh(config));
router
.route(`/${slug}/me`)
.get(me);
router
.route(`/${slug}/first-register`)
.post(registerFirstUser(config));
router
.route(`/${slug}/forgot-password`)
.post(forgotPassword(config, sendEmail));
router
.route(`${slug}/reset-password`)
.post(resetPassword);
router
.route(`/${slug}/register`)
.post(register(config));
router
.route(`/${slug}`)
.get(find);
router.route(`/${slug}/:id`)
.get(findByID)
.put(update)
.delete(deleteHandler);
return router;
};
module.exports = authRoutes;

View File

@@ -1,20 +1,36 @@
const PassportAPIKey = require('passport-headerapikey').HeaderAPIKeyStrategy; const PassportAPIKey = require('passport-headerapikey').HeaderAPIKeyStrategy;
module.exports = ({ Model, config }) => { module.exports = ({ operations }, { Model, config }) => {
const opts = { const opts = {
header: 'Authorization', header: 'Authorization',
prefix: `${config.labels.singular} API-Key `, prefix: `${config.labels.singular} API-Key `,
}; };
return new PassportAPIKey(opts, false, (apiKey, done) => { return new PassportAPIKey(opts, true, async (req, apiKey, done) => {
Model.findOne({ apiKey, enableAPIKey: true }, (err, user) => { try {
if (err) return done(err); const userQuery = await operations.collections.find({
if (!user) return done(null, false); where: {
apiKey: {
equals: apiKey,
},
},
collection: {
Model,
config,
},
req,
overrideAccess: true,
});
const json = user.toJSON({ virtuals: true }); if (userQuery.docs && userQuery.docs.length > 0) {
json.collection = config.slug; const user = userQuery.docs[0];
user.collection = config.slug;
return done(null, json); done(null, user);
}); } else {
done(null, false);
}
} catch (err) {
done(null, false);
}
}); });
}; };

View File

@@ -1,25 +1,44 @@
const passportJwt = require('passport-jwt'); const passportJwt = require('passport-jwt');
const getExtractJWT = require('../getExtractJWT');
const JwtStrategy = passportJwt.Strategy; const JwtStrategy = passportJwt.Strategy;
const { ExtractJwt } = passportJwt;
module.exports = (config, collections) => { module.exports = ({ config, collections, operations }) => {
const opts = {}; const opts = {
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme('JWT'); session: false,
passReqToCallback: true,
};
const extractJWT = getExtractJWT(config);
opts.jwtFromRequest = extractJWT;
opts.secretOrKey = config.secret; opts.secretOrKey = config.secret;
return new JwtStrategy(opts, async (token, done) => { return new JwtStrategy(opts, async (req, token, done) => {
try { try {
const collection = collections[token.collection]; const collection = collections[token.collection];
const user = await collection.Model.findByUsername(token.email); const userQuery = await operations.collections.find({
where: {
email: {
equals: token.email,
},
},
collection,
req,
overrideAccess: true,
});
const json = user.toJSON({ virtuals: true }); if (userQuery.docs && userQuery.docs.length > 0) {
json.collection = collection.config.slug; const user = userQuery.docs[0];
user.collection = collection.config.slug;
return done(null, json); done(null, user);
} else {
done(null, false);
}
} catch (err) { } catch (err) {
return done(null, false); done(null, false);
} }
}); });
}; };

View File

@@ -4,12 +4,14 @@
const webpack = require('webpack'); const webpack = require('webpack');
const getWebpackProdConfig = require('../webpack/getWebpackProdConfig'); const getWebpackProdConfig = require('../webpack/getWebpackProdConfig');
const findConfig = require('../utilities/findConfig'); const findConfig = require('../utilities/findConfig');
const sanitizeConfig = require('../utilities/sanitizeConfig');
module.exports = () => { module.exports = () => {
const configPath = findConfig(); const configPath = findConfig();
try { try {
const config = require(configPath); const unsanitizedConfig = require(configPath);
const config = sanitizeConfig(unsanitizedConfig);
const webpackProdConfig = getWebpackProdConfig({ const webpackProdConfig = getWebpackProdConfig({
...config, ...config,

View File

@@ -1,24 +1,9 @@
import Cookies from 'universal-cookie';
import qs from 'qs'; import qs from 'qs';
import config from 'payload/config';
const { cookiePrefix } = config;
const cookieTokenName = `${cookiePrefix}-token`;
export const getJWTHeader = () => {
const cookies = new Cookies();
const jwt = cookies.get(cookieTokenName);
return jwt ? { Authorization: `JWT ${jwt}` } : {};
};
export const requests = { export const requests = {
get: (url, params) => { get: (url, params) => {
const query = qs.stringify(params, { addQueryPrefix: true, depth: 10 }); const query = qs.stringify(params, { addQueryPrefix: true, depth: 10 });
return fetch(`${url}${query}`, { return fetch(`${url}${query}`);
headers: {
...getJWTHeader(),
},
});
}, },
post: (url, options = {}) => { post: (url, options = {}) => {
@@ -29,7 +14,6 @@ export const requests = {
method: 'post', method: 'post',
headers: { headers: {
...headers, ...headers,
...getJWTHeader(),
}, },
}; };
@@ -44,7 +28,6 @@ export const requests = {
method: 'put', method: 'put',
headers: { headers: {
...headers, ...headers,
...getJWTHeader(),
}, },
}; };
@@ -58,7 +41,6 @@ export const requests = {
method: 'delete', method: 'delete',
headers: { headers: {
...headers, ...headers,
...getJWTHeader(),
}, },
}); });
}, },

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
Route, Switch, withRouter, Redirect, Route, Switch, withRouter, Redirect, useHistory,
} from 'react-router-dom'; } from 'react-router-dom';
import config from 'payload/config'; import config from 'payload/config';
import List from './views/collections/List'; import List from './views/collections/List';
@@ -25,17 +25,22 @@ const {
} = config; } = config;
const Routes = () => { const Routes = () => {
const history = useHistory();
const [initialized, setInitialized] = useState(null); const [initialized, setInitialized] = useState(null);
const { user, permissions, permissions: { canAccessAdmin } } = useUser(); const { user, permissions, permissions: { canAccessAdmin } } = useUser();
useEffect(() => { useEffect(() => {
requests.get(`${routes.api}/${userSlug}/init`).then(res => res.json().then((data) => { requests.get(`${routes.api}/${userSlug}/init`).then((res) => res.json().then((data) => {
if (data && 'initialized' in data) { if (data && 'initialized' in data) {
setInitialized(data.initialized); setInitialized(data.initialized);
} }
})); }));
}, []); }, []);
useEffect(() => {
history.replace();
}, [history]);
return ( return (
<Route <Route
path={routes.admin} path={routes.admin}
@@ -62,6 +67,9 @@ const Routes = () => {
<Route path={`${match.url}/logout`}> <Route path={`${match.url}/logout`}>
<Logout /> <Logout />
</Route> </Route>
<Route path={`${match.url}/logout-inactivity`}>
<Logout inactivity />
</Route>
<Route path={`${match.url}/forgot`}> <Route path={`${match.url}/forgot`}>
<ForgotPassword /> <ForgotPassword />
</Route> </Route>
@@ -94,14 +102,12 @@ const Routes = () => {
key={`${collection.slug}-list`} key={`${collection.slug}-list`}
path={`${match.url}/collections/${collection.slug}`} path={`${match.url}/collections/${collection.slug}`}
exact exact
render={(routeProps) => { render={(routeProps) => (
return ( <List
<List {...routeProps}
{...routeProps} collection={collection}
collection={collection} />
/> )}
);
}}
/> />
); );
} }
@@ -116,14 +122,12 @@ const Routes = () => {
key={`${collection.slug}-create`} key={`${collection.slug}-create`}
path={`${match.url}/collections/${collection.slug}/create`} path={`${match.url}/collections/${collection.slug}/create`}
exact exact
render={(routeProps) => { render={(routeProps) => (
return ( <Edit
<Edit {...routeProps}
{...routeProps} collection={collection}
collection={collection} />
/> )}
);
}}
/> />
); );
} }
@@ -138,15 +142,13 @@ const Routes = () => {
key={`${collection.slug}-edit`} key={`${collection.slug}-edit`}
path={`${match.url}/collections/${collection.slug}/:id`} path={`${match.url}/collections/${collection.slug}/:id`}
exact exact
render={(routeProps) => { render={(routeProps) => (
return ( <Edit
<Edit isEditing
isEditing {...routeProps}
{...routeProps} collection={collection}
collection={collection} />
/> )}
);
}}
/> />
); );
} }
@@ -161,14 +163,12 @@ const Routes = () => {
key={`${global.slug}`} key={`${global.slug}`}
path={`${match.url}/globals/${global.slug}`} path={`${match.url}/globals/${global.slug}`}
exact exact
render={(routeProps) => { render={(routeProps) => (
return ( <EditGlobal
<EditGlobal {...routeProps}
{...routeProps} global={global}
global={global} />
/> )}
);
}}
/> />
); );
} }
@@ -189,6 +189,10 @@ const Routes = () => {
return <Loading />; return <Loading />;
} }
if (user === undefined) {
return <Loading />;
}
return <Redirect to={`${match.url}/login`} />; return <Redirect to={`${match.url}/login`} />;
}} }}
/> />

View File

@@ -24,9 +24,9 @@ function recursivelyAddFieldComponents(fields) {
}; };
} }
if (field.components || field.fields) { if (field.admin.components || field.fields) {
const fieldComponents = { const fieldComponents = {
...(field.components || {}), ...(field.admin.components || {}),
}; };
if (field.fields) { if (field.fields) {
@@ -56,7 +56,7 @@ function customComponents(config) {
newComponents[collection.slug] = { newComponents[collection.slug] = {
fields: recursivelyAddFieldComponents(collection.fields), fields: recursivelyAddFieldComponents(collection.fields),
...(collection.components || {}), ...(collection.admin.components || {}),
}; };
return newComponents; return newComponents;
@@ -67,7 +67,7 @@ function customComponents(config) {
newComponents[global.slug] = { newComponents[global.slug] = {
fields: recursivelyAddFieldComponents(global.fields), fields: recursivelyAddFieldComponents(global.fields),
...(global.components || {}), ...(global.admin.components || {}),
}; };
return newComponents; return newComponents;
@@ -76,7 +76,7 @@ function customComponents(config) {
const string = stringify({ const string = stringify({
...(allCollectionComponents || {}), ...(allCollectionComponents || {}),
...(allGlobalComponents || {}), ...(allGlobalComponents || {}),
...(config.components || {}), ...(config.admin.components || {}),
}).replace(/\\/g, '\\\\'); }).replace(/\\/g, '\\\\');
return { return {

View File

@@ -1,18 +1,16 @@
import React, { import React, {
useState, createContext, useContext, useEffect, useCallback, useState, createContext, useContext, useEffect, useCallback,
} from 'react'; } from 'react';
import { useLocation, useHistory } from 'react-router-dom';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { useLocation, useHistory } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Cookies from 'universal-cookie';
import config from 'payload/config'; import config from 'payload/config';
import { useModal } from '@trbl/react-modal'; import { useModal } from '@faceless-ui/modal';
import { requests } from '../../api'; import { requests } from '../../api';
import StayLoggedInModal from '../modals/StayLoggedIn'; import StayLoggedInModal from '../modals/StayLoggedIn';
import useDebounce from '../../hooks/useDebounce'; import useDebounce from '../../hooks/useDebounce';
const { const {
cookiePrefix,
admin: { admin: {
user: userSlug, user: userSlug,
}, },
@@ -23,16 +21,12 @@ const {
}, },
} = config; } = config;
const cookieTokenName = `${cookiePrefix}-token`;
const cookies = new Cookies();
const Context = createContext({}); const Context = createContext({});
const isNotExpired = decodedJWT => (decodedJWT?.exp || 0) > Date.now() / 1000;
const UserProvider = ({ children }) => { const UserProvider = ({ children }) => {
const [token, setToken] = useState(''); const [user, setUser] = useState(undefined);
const [user, setUser] = useState(null); const [tokenInMemory, setTokenInMemory] = useState(null);
const exp = user?.exp;
const [permissions, setPermissions] = useState({ canAccessAdmin: null }); const [permissions, setPermissions] = useState({ canAccessAdmin: null });
@@ -42,64 +36,72 @@ const UserProvider = ({ children }) => {
const [lastLocationChange, setLastLocationChange] = useState(0); const [lastLocationChange, setLastLocationChange] = useState(0);
const debouncedLocationChange = useDebounce(lastLocationChange, 10000); const debouncedLocationChange = useDebounce(lastLocationChange, 10000);
const exp = user?.exp || 0;
const email = user?.email; const email = user?.email;
const refreshToken = useCallback(() => { const refreshCookie = useCallback(() => {
// Need to retrieve token straight from cookie so as to keep this function const now = Math.round((new Date()).getTime() / 1000);
// with no dependencies and to make sure we have the exact token that will be used const remainingTime = (exp || 0) - now;
// in the request to the /refresh route
const tokenFromCookie = cookies.get(cookieTokenName);
const decodedToken = jwt.decode(tokenFromCookie);
if (decodedToken?.exp > (Date.now() / 1000)) { if (exp && remainingTime < 120) {
setTimeout(async () => { setTimeout(async () => {
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`); const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`);
if (request.status === 200) { if (request.status === 200) {
const json = await request.json(); const json = await request.json();
setToken(json.refreshedToken); setUser(json.user);
} else {
history.push(`${admin}/logout-inactivity`);
} }
}, 1000); }, 1000);
} }
}, [setToken]); }, [setUser, history, exp]);
const setToken = useCallback((token) => {
const decoded = jwt.decode(token);
setUser(decoded);
setTokenInMemory(token);
}, []);
const logOut = () => { const logOut = () => {
setUser(null); setUser(null);
setToken(null); setTokenInMemory(null);
cookies.remove(cookieTokenName, { path: '/' }); requests.get(`${serverURL}${api}/${userSlug}/logout`);
}; };
// On mount, get cookie and set as token // On mount, get user and set
useEffect(() => { useEffect(() => {
const cookieToken = cookies.get(cookieTokenName); const fetchMe = async () => {
if (cookieToken) setToken(cookieToken); const request = await requests.get(`${serverURL}${api}/${userSlug}/me`);
}, []);
// When location changes, refresh token if (request.status === 200) {
const json = await request.json();
setUser(json?.user || null);
if (json?.token) {
setToken(json.token);
}
}
};
fetchMe();
}, [setToken]);
// When location changes, refresh cookie
useEffect(() => { useEffect(() => {
refreshToken(); if (email) {
}, [debouncedLocationChange, refreshToken]); refreshCookie();
}
}, [debouncedLocationChange, refreshCookie, email]);
useEffect(() => { useEffect(() => {
setLastLocationChange(Date.now()); setLastLocationChange(Date.now());
}, [pathname]); }, [pathname]);
// When token changes, set cookie, decode and set user // When user changes, get new access
useEffect(() => {
if (token) {
const decoded = jwt.decode(token);
if (isNotExpired(decoded)) {
setUser(decoded);
cookies.set(cookieTokenName, token, { path: '/' });
}
}
}, [token]);
// When user changes, get new policies
useEffect(() => { useEffect(() => {
async function getPermissions() { async function getPermissions() {
const request = await requests.get(`${serverURL}${api}/policies`); const request = await requests.get(`${serverURL}${api}/access`);
if (request.status === 200) { if (request.status === 200) {
const json = await request.json(); const json = await request.json();
@@ -135,7 +137,6 @@ const UserProvider = ({ children }) => {
if (remainingTime > 0) { if (remainingTime > 0) {
forceLogOut = setTimeout(() => { forceLogOut = setTimeout(() => {
logOut();
history.push(`${admin}/logout`); history.push(`${admin}/logout`);
closeAllModals(); closeAllModals();
}, remainingTime * 1000); }, remainingTime * 1000);
@@ -149,15 +150,15 @@ const UserProvider = ({ children }) => {
return ( return (
<Context.Provider value={{ <Context.Provider value={{
user, user,
setToken,
logOut, logOut,
refreshToken, refreshCookie,
token,
permissions, permissions,
setToken,
token: tokenInMemory,
}} }}
> >
{children} {children}
<StayLoggedInModal refreshToken={refreshToken} /> <StayLoggedInModal refreshCookie={refreshCookie} />
</Context.Provider> </Context.Provider>
); );
}; };

View File

@@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from '../Button';
import './index.scss';
const baseClass = 'card';
const Card = (props) => {
const { title, actions, onClick } = props;
const classes = [
baseClass,
onClick && `${baseClass}--has-onclick`,
].filter(Boolean).join(' ');
return (
<div className={classes}>
<h5>
{title}
</h5>
{actions && (
<div className={`${baseClass}__actions`}>
{actions}
</div>
)}
{onClick && (
<Button
className={`${baseClass}__click`}
buttonStyle="none"
onClick={onClick}
/>
)}
</div>
);
};
Card.defaultProps = {
actions: null,
onClick: undefined,
};
Card.propTypes = {
title: PropTypes.string.isRequired,
actions: PropTypes.node,
onClick: PropTypes.func,
};
export default Card;

View File

@@ -0,0 +1,41 @@
@import '../../../scss/styles';
.card {
background: $color-background-gray;
padding: base(1.25) $baseline;
position: relative;
h5 {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__actions {
position: relative;
z-index: 2;
margin-top: base(.5);
display: inline-flex;
.btn {
margin: 0;
}
}
&--has-onclick {
cursor: pointer;
&:hover {
background: darken($color-background-gray, 3%);
}
}
&__click {
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}

View File

@@ -1,10 +1,12 @@
const getInitialColumnState = (fields, useAsTitle, defaultColumns) => { const getInitialColumnState = (fields, useAsTitle, defaultColumns) => {
let initialColumns = []; let initialColumns = [];
const hasThumbnail = fields.find(field => field.type === 'thumbnail'); const hasThumbnail = fields.find((field) => field.type === 'thumbnail');
if (Array.isArray(defaultColumns)) { if (Array.isArray(defaultColumns) && defaultColumns.length >= 1) {
initialColumns = defaultColumns; return {
columns: defaultColumns,
};
} }
if (hasThumbnail) { if (hasThumbnail) {
@@ -15,11 +17,8 @@ const getInitialColumnState = (fields, useAsTitle, defaultColumns) => {
initialColumns.push(useAsTitle); initialColumns.push(useAsTitle);
} }
const remainingColumns = fields.filter((field) => { const remainingColumns = fields.filter((field) => field.name !== useAsTitle && field.type !== 'thumbnail')
return field.name !== useAsTitle && field.type !== 'thumbnail'; .slice(0, 3 - initialColumns.length).map((field) => field.name);
}).slice(0, 3 - initialColumns.length).map((field) => {
return field.name;
});
initialColumns = initialColumns.concat(remainingColumns); initialColumns = initialColumns.concat(remainingColumns);

View File

@@ -23,17 +23,19 @@ const reducer = (state, { type, payload }) => {
]; ];
} }
return state.filter(remainingColumn => remainingColumn !== payload); return state.filter((remainingColumn) => remainingColumn !== payload);
}; };
const ColumnSelector = (props) => { const ColumnSelector = (props) => {
const { const {
collection: { collection: {
fields, fields,
useAsTitle, admin: {
useAsTitle,
defaultColumns,
},
}, },
handleChange, handleChange,
defaultColumns,
} = props; } = props;
const [initialColumns, setInitialColumns] = useState([]); const [initialColumns, setInitialColumns] = useState([]);
@@ -55,7 +57,7 @@ const ColumnSelector = (props) => {
return ( return (
<div className={baseClass}> <div className={baseClass}>
{fields && fields.map((field, i) => { {fields && fields.map((field, i) => {
const isEnabled = columns.find(column => column === field.name); const isEnabled = columns.find((column) => column === field.name);
return ( return (
<Pill <Pill
onClick={() => dispatchColumns({ payload: field.name, type: isEnabled ? 'disable' : 'enable' })} onClick={() => dispatchColumns({ payload: field.name, type: isEnabled ? 'disable' : 'enable' })}
@@ -73,20 +75,18 @@ const ColumnSelector = (props) => {
); );
}; };
ColumnSelector.defaultProps = {
defaultColumns: undefined,
};
ColumnSelector.propTypes = { ColumnSelector.propTypes = {
collection: PropTypes.shape({ collection: PropTypes.shape({
fields: PropTypes.arrayOf( fields: PropTypes.arrayOf(
PropTypes.shape({}), PropTypes.shape({}),
), ),
useAsTitle: PropTypes.string, admin: PropTypes.shape({
defaultColumns: PropTypes.arrayOf(
PropTypes.string,
),
useAsTitle: PropTypes.string,
}),
}).isRequired, }).isRequired,
defaultColumns: PropTypes.arrayOf(
PropTypes.string,
),
handleChange: PropTypes.func.isRequired, handleChange: PropTypes.func.isRequired,
}; };

View File

@@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import config from 'payload/config'; import config from 'payload/config';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { Modal, useModal } from '@trbl/react-modal'; import { Modal, useModal } from '@faceless-ui/modal';
import Button from '../Button'; import Button from '../Button';
import MinimalTemplate from '../../templates/Minimal'; import MinimalTemplate from '../../templates/Minimal';
import useTitle from '../../../hooks/useTitle'; import useTitle from '../../../hooks/useTitle';
@@ -20,7 +20,9 @@ const DeleteDocument = (props) => {
title: titleFromProps, title: titleFromProps,
id, id,
collection: { collection: {
useAsTitle, admin: {
useAsTitle,
},
slug, slug,
labels: { labels: {
singular, singular,
@@ -80,7 +82,7 @@ const DeleteDocument = (props) => {
if (id) { if (id) {
return ( return (
<> <React.Fragment>
<button <button
type="button" type="button"
slug={modalSlug} slug={modalSlug}
@@ -127,7 +129,7 @@ const DeleteDocument = (props) => {
</Button> </Button>
</MinimalTemplate> </MinimalTemplate>
</Modal> </Modal>
</> </React.Fragment>
); );
} }
@@ -141,7 +143,9 @@ DeleteDocument.defaultProps = {
DeleteDocument.propTypes = { DeleteDocument.propTypes = {
collection: PropTypes.shape({ collection: PropTypes.shape({
useAsTitle: PropTypes.string, admin: PropTypes.shape({
useAsTitle: PropTypes.string,
}),
slug: PropTypes.string, slug: PropTypes.string,
labels: PropTypes.shape({ labels: PropTypes.shape({
singular: PropTypes.string, singular: PropTypes.string,

View File

@@ -1,8 +1,9 @@
import React from 'react'; import React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Link } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import config from 'payload/config'; import config from 'payload/config';
import useForm from '../../forms/Form/useForm'; import Button from '../Button';
import { useForm } from '../../forms/Form/context';
import './index.scss'; import './index.scss';
@@ -11,21 +12,28 @@ const { routes: { admin } } = config;
const baseClass = 'duplicate'; const baseClass = 'duplicate';
const Duplicate = ({ slug }) => { const Duplicate = ({ slug }) => {
const { push } = useHistory();
const { getData } = useForm(); const { getData } = useForm();
const data = getData();
const handleClick = useCallback(() => {
const data = getData();
push({
pathname: `${admin}/collections/${slug}/create`,
state: {
data,
},
});
}, [push, getData, slug]);
return ( return (
<Link <Button
buttonStyle="none"
className={baseClass} className={baseClass}
to={{ onClick={handleClick}
pathname: `${admin}/collections/${slug}/create`,
state: {
data,
},
}}
> >
Duplicate Duplicate
</Link> </Button>
); );
}; };

View File

@@ -91,6 +91,7 @@
&__main-detail { &__main-detail {
border-top: $style-stroke-width-m solid white; border-top: $style-stroke-width-m solid white;
order: 3; order: 3;
width: 100%;
} }
} }
} }

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