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 = {
parser: "babel-eslint",
env: {
browser: true,
es6: true,
jest: true,
},
extends: 'airbnb',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: [
'react',
'react-hooks',
],
extends: "@trbl",
rules: {
"import/no-unresolved": [
2,
@@ -25,56 +9,7 @@ module.exports = {
'payload/config',
'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',
label: 'Author',
type: 'text',
maxLength: 100,
type: 'relationship',
relationTo: 'public-users',
required: true,
},
{

View File

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

View File

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

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',
label: 'Enable Test is checked',
required: true,
condition: (_, siblings) => siblings.enableTest === true,
admin: {
condition: (_, siblings) => siblings.enableTest === true,
},
},
{
name: 'orCondition',
type: 'text',
label: 'Number is greater than 20 OR enableTest is checked',
required: true,
condition: (_, siblings) => siblings.number > 20 || siblings.enableTest === true,
admin: {
condition: (_, siblings) => siblings.number > 20 || siblings.enableTest === true,
},
},
{
name: 'nestedConditions',
type: 'text',
label: 'Number is either greater than 20 AND enableTest is checked, OR number is less than 20 and enableTest is NOT checked',
condition: (_, siblings) => {
return (siblings.number > 20 && siblings.enableTest === true) || (siblings.number < 20 && siblings.enableTest === false);
admin: {
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',
plural: 'Custom Components',
},
useAsTitle: 'title',
fields: [
{
name: 'title',
@@ -16,11 +15,6 @@ module.exports = {
required: true,
unique: true,
localized: true,
hooks: {
beforeCreate: operation => operation.value,
beforeUpdate: operation => operation.value,
afterRead: operation => operation.value,
},
},
{
name: 'description',
@@ -29,23 +23,27 @@ module.exports = {
height: 100,
required: true,
localized: true,
components: {
field: path.resolve(__dirname, 'components/fields/Description/Field/index.js'),
cell: path.resolve(__dirname, 'components/fields/Description/Cell/index.js'),
filter: path.resolve(__dirname, 'components/fields/Description/Filter/index.js'),
admin: {
components: {
field: path.resolve(__dirname, 'components/fields/Description/Field/index.js'),
cell: path.resolve(__dirname, 'components/fields/Description/Cell/index.js'),
filter: path.resolve(__dirname, 'components/fields/Description/Filter/index.js'),
},
},
},
{
name: 'repeater',
label: 'Repeater',
type: 'repeater',
name: 'array',
label: 'Array',
type: 'array',
fields: [
{
type: 'text',
name: 'nestedRepeaterCustomField',
label: 'Nested Repeater Custom Field',
components: {
field: path.resolve(__dirname, 'components/fields/NestedRepeaterCustomField/Field/index.js'),
name: 'nestedArrayCustomField',
label: 'Nested Array Custom Field',
admin: {
components: {
field: path.resolve(__dirname, 'components/fields/NestedArrayCustomField/Field/index.js'),
},
},
},
],
@@ -54,16 +52,20 @@ module.exports = {
name: 'group',
label: 'Group',
type: 'group',
components: {
field: path.resolve(__dirname, 'components/fields/Group/Field/index.js'),
admin: {
components: {
field: path.resolve(__dirname, 'components/fields/Group/Field/index.js'),
},
},
fields: [
{
type: 'text',
name: 'nestedGroupCustomField',
label: 'Nested Group Custom Field',
components: {
field: path.resolve(__dirname, 'components/fields/NestedGroupCustomField/Field/index.js'),
admin: {
components: {
field: path.resolve(__dirname, 'components/fields/NestedGroupCustomField/Field/index.js'),
},
},
},
],
@@ -75,8 +77,10 @@ module.exports = {
name: 'nestedText1',
label: 'Nested Text 1',
type: 'text',
components: {
field: path.resolve(__dirname, 'components/fields/NestedText1/Field/index.js'),
admin: {
components: {
field: path.resolve(__dirname, 'components/fields/NestedText1/Field/index.js'),
},
},
}, {
name: 'nestedText2',
@@ -87,9 +91,12 @@ module.exports = {
},
],
timestamps: true,
components: {
views: {
List: path.resolve(__dirname, 'components/views/List/index.js'),
admin: {
useAsTitle: 'title',
components: {
views: {
List: path.resolve(__dirname, 'components/views/List/index.js'),
},
},
},
};

View File

@@ -1,7 +1,7 @@
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);
if (isAdmin) {
@@ -27,13 +27,12 @@ module.exports = {
staticURL: '/files',
staticDir: path.resolve(__dirname, '../files'),
},
policies: {
access: {
create: () => true,
read: policy,
update: policy,
delete: policy,
read: access,
update: access,
delete: access,
},
useAsTitle: 'filename',
fields: [
{
name: 'type',
@@ -61,4 +60,7 @@ module.exports = {
},
],
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',
plural: 'Hooks',
},
useAsTitle: 'title',
policies: {
admin: {
useAsTitle: 'title',
},
access: {
create: () => true,
read: () => true,
update: () => true,
delete: () => true,
},
hooks: {
beforeCreate: (operation) => {
if (operation.req.headers.hook === 'beforeCreate') {
operation.req.body.description += '-beforeCreateSuffix';
}
return operation;
},
beforeRead: (operation) => {
if (operation.req.headers.hook === 'beforeRead') {
operation.limit = 1;
}
return operation;
},
beforeUpdate: (operation) => {
if (operation.req.headers.hook === 'beforeUpdate') {
operation.req.body.description += '-beforeUpdateSuffix';
}
return operation;
},
beforeDelete: (operation) => {
if (operation.req.headers.hook === 'beforeDelete') {
// TODO: Find a better hook operation to assert against in tests
operation.req.headers.hook = 'afterDelete';
}
return operation;
},
afterCreate: (operation, value) => {
if (operation.req.headers.hook === 'afterCreate') {
value.afterCreateHook = true;
}
return value;
},
afterRead: (operation) => {
const { doc } = operation;
doc.afterReadHook = true;
beforeCreate: [
(operation) => {
if (operation.req.headers.hook === 'beforeCreate') {
operation.req.body.description += '-beforeCreateSuffix';
}
return operation.data;
},
],
beforeRead: [
(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';
}
return operation.data;
},
],
beforeDelete: [
(operation) => {
if (operation.req.headers.hook === 'beforeDelete') {
// TODO: Find a better hook operation to assert against in tests
operation.req.headers.hook = 'afterDelete';
}
},
],
afterCreate: [
(operation) => {
if (operation.req.headers.hook === 'afterCreate') {
operation.doc.afterCreateHook = true;
}
return operation.doc;
},
],
afterRead: [
(operation) => {
const { doc } = operation;
doc.afterReadHook = true;
return doc;
},
afterUpdate: (operation, value) => {
if (operation.req.headers.hook === 'afterUpdate') {
value.afterUpdateHook = true;
}
return value;
},
afterDelete: (operation, value) => {
if (operation.req.headers.hook === 'afterDelete') {
value.afterDeleteHook = true;
}
return value;
},
return doc;
},
],
afterUpdate: [
(operation) => {
if (operation.req.headers.hook === 'afterUpdate') {
operation.doc.afterUpdateHook = true;
}
return operation.doc;
},
],
afterDelete: [
(operation) => {
if (operation.req.headers.hook === 'afterDelete') {
operation.doc.afterDeleteHook = true;
}
return operation.doc;
},
],
},
fields: [
{
@@ -73,7 +89,9 @@ module.exports = {
unique: true,
localized: true,
hooks: {
afterRead: value => (value ? value.toUpperCase() : null),
afterRead: [
({ value }) => (value ? value.toUpperCase() : null),
],
},
},
{

View File

@@ -4,22 +4,24 @@ module.exports = {
singular: 'Localized Post',
plural: 'Localized Posts',
},
useAsTitle: 'title',
policies: {
admin: {
useAsTitle: 'title',
defaultColumns: [
'title',
'priority',
'createdAt',
],
},
access: {
read: () => true,
},
preview: (doc, token) => {
if (doc.title) {
if (doc && doc.title) {
return `http://localhost:3000/posts/${doc.title.value}?preview=true&token=${token}`;
}
return null;
},
defaultColumns: [
'title',
'priority',
'createdAt',
],
fields: [
{
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 checkRole = require('../access/checkRole');
module.exports = {
slug: 'media',
@@ -6,7 +7,7 @@ module.exports = {
singular: 'Media',
plural: 'Media',
},
policies: {
access: {
read: () => true,
},
upload: {
@@ -40,6 +41,25 @@ module.exports = {
type: 'text',
required: 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,

View File

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

View File

@@ -4,7 +4,9 @@ module.exports = {
singular: 'Previewable Post',
plural: 'Previewable Posts',
},
useAsTitle: 'title',
admin: {
useAsTitle: 'title',
},
preview: (doc, token) => {
if (doc.title) {
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 = {
slug: 'public-users',
@@ -8,8 +8,10 @@ module.exports = {
singular: 'Public User',
plural: 'Public Users',
},
useAsTitle: 'email',
policies: {
admin: {
useAsTitle: 'email',
},
access: {
admin: () => false,
create: () => true,
read: () => true,
@@ -30,6 +32,7 @@ module.exports = {
},
auth: {
tokenExpiration: 300,
secureCookie: process.env.NODE_ENV === 'production',
},
fields: [
{
@@ -37,10 +40,10 @@ module.exports = {
label: 'This field should only be readable and editable by Admins with "admin" role',
type: 'text',
defaultValue: 'test',
policies: {
create: policy,
read: policy,
update: policy,
access: {
create: access,
read: access,
update: access,
},
},
],

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ module.exports = {
singular: 'Validation',
plural: 'Validations',
},
policies: {
access: {
read: () => true,
},
fields: [
@@ -51,7 +51,7 @@ module.exports = {
],
},
{
type: 'repeater',
type: 'array',
label: 'Should have at least 3 rows',
name: 'atLeast3Rows',
required: true,
@@ -59,7 +59,7 @@ module.exports = {
const result = value && value.length >= 3;
if (!result) {
return 'This repeater needs to have at least 3 rows.';
return 'This array needs to have at least 3 rows.';
}
return true;
@@ -83,9 +83,9 @@ module.exports = {
],
},
{
type: 'repeater',
label: 'Default repeater validation',
name: 'repeater',
type: 'array',
label: 'Default array validation',
name: 'array',
required: true,
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 = {
slug: 'navigation-repeater',
label: 'Navigation Repeater',
policies: {
slug: 'navigation-array',
label: 'Navigation Array',
access: {
update: ({ req: { user } }) => checkRole(['admin', 'user'], user),
read: () => true,
},
fields: [
{
name: 'repeater',
label: 'Repeater',
type: 'repeater',
name: 'array',
label: 'Array',
type: 'array',
localized: true,
fields: [{
name: 'text',

View File

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

View File

@@ -1,6 +1,8 @@
/* eslint-disable no-console */
const express = require('express');
const path = require('path');
const Payload = require('../src');
const logger = require('../src/utilities/logger')();
const expressApp = express();
@@ -13,19 +15,37 @@ const payload = new Payload({
secret: 'SECRET_KEY',
mongoURL: 'mongodb://localhost/payload',
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.start = (cb) => {
const server = expressApp.listen(3000, async () => {
console.log(`listening on ${3000}...`);
logger.info(`listening on ${3000}...`);
if (cb) cb();
const creds = await payload.getMockEmailCredentials();
console.log(`Mock email account username: ${creds.user}`);
console.log(`Mock email account password: ${creds.pass}`);
console.log(`Log in to mock email provider at ${creds.web}`);
logger.info(`Mock email account username: ${creds.user}`);
logger.info(`Mock email account password: ${creds.pass}`);
logger.info(`Log in to mock email provider at ${creds.web}`);
});
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 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",
"version": "1.0.0",
"description": "",
"name": "@payloadcms/payload",
"version": "0.0.28",
"description": "CMS and Application Framework",
"license": "ISC",
"author": "Payload CMS LLC",
"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": {
"payload": "./src/bin/index.js"
},
"author": "",
"license": "ISC",
"scripts": {
"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": {
"@babel/core": "^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/preset-env": "^7.8.3",
"@babel/preset-react": "^7.8.3",
"@babel/runtime": "^7.8.4",
"@date-io/date-fns": "^1.3.13",
"@trbl/react-collapsibles": "^0.1.0",
"@trbl/react-modal": "^1.0.4",
"@trbl/react-scroll-info": "^1.1.1",
"@trbl/react-window-info": "^1.2.2",
"@faceless-ui/collapsibles": "^0.1.0",
"@faceless-ui/modal": "^1.0.4",
"@faceless-ui/scroll-info": "^1.1.1",
"@faceless-ui/window-info": "^1.2.2",
"@udecode/slate-plugins": "^0.60.0",
"async-some": "^1.0.2",
"autoprefixer": "^9.7.4",
"babel-core": "^7.0.0-bridge.0",
"babel-loader": "^8.0.6",
"body-parser": "^1.19.0",
"compression": "^1.7.4",
"connect-history-api-fallback": "^1.6.0",
"cookie-parser": "^1.4.5",
"css-loader": "^1.0.0",
"date-fns": "^2.14.0",
"deepmerge": "^4.2.2",
"dotenv": "^6.0.0",
@@ -44,34 +47,43 @@
"express": "^4.17.1",
"express-fileupload": "^1.1.6",
"express-graphql": "^0.9.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"falsey": "^1.0.0",
"file-loader": "^1.1.11",
"flatley": "^5.2.0",
"graphql": "^15.0.0",
"graphql-date": "^1.0.3",
"graphql-playground-middleware-express": "^1.7.14",
"graphql-type-json": "^0.3.1",
"html-webpack-plugin": "^3.2.0",
"http-status": "^1.4.2",
"image-size": "^0.7.5",
"is-hotkey": "^0.1.6",
"isomorphic-fetch": "^2.2.1",
"jest": "^25.3.0",
"jsonwebtoken": "^8.5.1",
"method-override": "^3.0.0",
"micro-memoize": "^4.0.9",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"mongodb-memory-server": "^6.5.2",
"mongoose": "^5.8.9",
"mongoose-autopopulate": "^0.11.0",
"mongoose-hidden": "^1.8.1",
"mongoose-paginate-v2": "^1.3.6",
"node-sass": "^4.13.1",
"node-sass-chokidar": "^1.4.0",
"nodemailer": "^6.4.2",
"object-to-formdata": "^3.0.9",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"passport": "^0.4.1",
"passport-anonymous": "^1.0.1",
"passport-headerapikey": "^1.2.1",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"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",
"prop-types": "^15.7.2",
"qs": "^6.9.1",
@@ -83,19 +95,19 @@
"react-document-meta": "^3.0.0-beta.2",
"react-dom": "^16.13.1",
"react-hook-form": "^5.7.2",
"react-redux": "^7.2.0",
"react-router-dom": "^5.1.2",
"react-router-navigation-prompt": "^1.8.11",
"react-select": "^3.0.8",
"sanitize-filename": "^1.6.3",
"sass-loader": "7.1.0",
"sharp": "^0.25.2",
"slate": "^0.58.3",
"slate-history": "^0.58.3",
"slate-hyperscript": "^0.58.3",
"slate-react": "^0.58.3",
"style-loader": "^0.21.0",
"styled-components": "^5.1.1",
"uglifyjs-webpack-plugin": "^1.3.0",
"universal-cookie": "^3.1.0",
"uglifyjs-webpack-plugin": "^2.2.0",
"url-loader": "^1.0.1",
"uuid": "^8.1.0",
"val-loader": "^2.1.0",
@@ -105,35 +117,19 @@
"webpack-hot-middleware": "^2.25.0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
"autoprefixer": "^9.7.4",
"babel-core": "^7.0.0-bridge.0",
"@trbl/eslint-config": "^1.2.4",
"babel-eslint": "^10.0.1",
"cross-env": "^7.0.2",
"css-loader": "^1.0.0",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-loader": "^4.0.2",
"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-react": "^7.18.0",
"eslint-plugin-react-hooks": "^2.3.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"faker": "^4.1.0",
"node-sass": "^4.13.1",
"node-sass-chokidar": "^1.4.0",
"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"
]
"graphql-request": "^2.0.0",
"nodemon": "^1.19.4"
}
}

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,9 @@ const defaultUser = {
singular: 'User',
plural: 'Users',
},
useAsTitle: 'email',
admin: {
useAsTitle: 'email',
},
auth: {
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 formatConfigNames = (results, configs) => {
@@ -13,18 +12,17 @@ const formatConfigNames = (results, configs) => {
return formattedResults;
};
const policyResolver = config => async (_, __, context) => {
async function access(_, __, context) {
const options = {
config,
req: context,
req: context.req,
};
let policyResults = await policies(options);
let accessResults = await this.operations.collections.auth.access(options);
policyResults = formatConfigNames(policyResults, config.collections);
policyResults = formatConfigNames(policyResults, config.globals);
accessResults = formatConfigNames(accessResults, this.config.collections);
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 */
const { forgotPassword } = require('../../operations');
function forgotPassword(collection) {
async function resolver(_, args, context) {
const options = {
collection,
data: args,
req: context.req,
};
const forgotPasswordResolver = (config, collection, email) => async (_, args, context) => {
const options = {
config,
collection,
data: args,
email,
req: context,
};
await this.operations.collections.auth.forgotPassword(options);
return true;
}
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 */
const { init } = require('../../operations');
function init({ Model }) {
async function resolver(_, __, context) {
const options = {
Model,
req: context.req,
};
const initResolver = ({ Model }) => async (_, __, context) => {
const options = {
Model,
req: context,
};
const result = await this.operations.collections.auth.init(options);
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 */
const { login } = require('../../operations');
function login(collection) {
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 options = {
collection,
config,
data: {
email: args.email,
password: args.password,
},
req: context,
};
const token = await this.operations.collections.auth.login(options);
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 */
const meResolver = async (_, args, context) => {
return context.user;
};
async function me(_, __, context) {
return this.operations.collections.auth.me({ req: context.req });
}
module.exports = meResolver;
module.exports = me;

View File

@@ -1,17 +1,24 @@
/* eslint-disable no-param-reassign */
const { refresh } = require('../../operations');
const getExtractJWT = require('../../getExtractJWT');
const refreshResolver = (config, collection) => async (_, __, context) => {
const options = {
config,
collection,
authorization: context.headers.authorization,
req: context,
};
function refresh(collection) {
async function resolver(_, __, context) {
const extractJWT = getExtractJWT(this.config);
const token = extractJWT(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 */
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) => {
const options = {
config,
collection,
data: args.data,
depth: 0,
req: context,
};
if (args.locale) {
context.req.locale = args.locale;
options.locale = args.locale;
}
if (args.locale) {
context.locale = args.locale;
options.locale = args.locale;
if (args.fallbackLocale) {
context.req.fallbackLocale = args.fallbackLocale;
options.fallbackLocale = args.fallbackLocale;
}
const token = await this.operations.collections.auth.register(options);
return token;
}
if (args.fallbackLocale) {
context.fallbackLocale = args.fallbackLocale;
options.fallbackLocale = args.fallbackLocale;
}
const registerResolver = resolver.bind(this);
return registerResolver;
}
const token = await register(options);
return token;
};
module.exports = registerResolver;
module.exports = register;

View File

@@ -1,22 +1,23 @@
/* 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) => {
if (args.locale) context.locale = args.locale;
if (args.fallbackLocale) context.fallbackLocale = args.fallbackLocale;
const options = {
collection,
data: args,
req: context.req,
api: 'GraphQL',
};
const options = {
collection,
config,
data: args,
req: context,
api: 'GraphQL',
user: context.user,
};
const token = await this.operations.collections.auth.resetPassword(options);
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 */
const { update } = require('../../operations');
const updateResolver = ({ Model, config }) => async (_, args, context) => {
if (args.locale) context.locale = args.locale;
if (args.fallbackLocale) context.fallbackLocale = args.fallbackLocale;
function update(collection) {
async function resolver(_, args, context) {
if (args.locale) context.req.locale = args.locale;
if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale;
const options = {
config,
Model,
data: args.data,
id: args.id,
depth: 0,
req: context,
};
const options = {
collection,
data: args.data,
id: args.id,
depth: 0,
req: context.req,
};
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 { APIError } = require('../../errors');
const forgotPassword = async (args) => {
try {
if (!Object.prototype.hasOwnProperty.call(args.data, 'email')) {
throw new APIError('Missing email.');
}
async function forgotPassword(args) {
const { config, sendEmail: email } = this;
let options = { ...args };
if (!Object.prototype.hasOwnProperty.call(args.data, 'email')) {
throw new APIError('Missing email.');
}
// /////////////////////////////////////
// 1. Execute before login hook
// /////////////////////////////////////
let options = { ...args };
const { beforeForgotPassword } = args.collection.config.hooks;
// /////////////////////////////////////
// 1. Execute before login hook
// /////////////////////////////////////
if (typeof beforeForgotPassword === 'function') {
options = await beforeForgotPassword(options);
}
const { beforeForgotPassword } = args.collection.config.hooks;
// /////////////////////////////////////
// 2. Perform forgot password
// /////////////////////////////////////
if (typeof beforeForgotPassword === 'function') {
options = await beforeForgotPassword(options);
}
const {
collection: {
Model,
},
config,
data,
email,
} = options;
// /////////////////////////////////////
// 2. Perform forgot password
// /////////////////////////////////////
let token = await crypto.randomBytes(20);
token = token.toString('hex');
const {
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;
user.resetPasswordExpiration = Date.now() + 3600000; // 1 hour
if (!user) return;
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:
<a href="${config.serverURL}${config.routes.admin}/reset/${token}">
${config.serverURL}${config.routes.admin}/reset/${token}
</a>
If you did not request this, please ignore this email and your password will remain unchanged.`;
email({
from: `"${config.email.fromName}" <${config.email.fromAddress}>`,
to: data.email,
subject: 'Password Reset',
html,
});
email({
from: `"${config.email.fromName}" <${config.email.fromAddress}>`,
to: data.email,
subject: 'Password Reset',
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') {
await afterForgotPassword(options);
}
} catch (error) {
throw error;
if (typeof afterForgotPassword === 'function') {
await afterForgotPassword(options);
}
};
}
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) => {
try {
const {
Model,
} = args;
async function init(args) {
const {
Model,
} = args;
const count = await Model.countDocuments({});
const count = await Model.countDocuments({});
if (count >= 1) return true;
if (count >= 1) return true;
return false;
} catch (error) {
throw error;
}
};
return false;
}
module.exports = init;

View File

@@ -1,88 +1,112 @@
const jwt = require('jsonwebtoken');
const { Unauthorized, AuthenticationError } = require('../../errors');
const { AuthenticationError } = require('../../errors');
const login = async (args) => {
try {
// Await validation here
async function login(args) {
const { config, operations } = this;
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
// /////////////////////////////////////
// /////////////////////////////////////
// 2. Perform login
// /////////////////////////////////////
const {
collection: {
Model,
config: collectionConfig,
},
data,
req,
} = options;
const {
collection: {
Model,
config: collectionConfig,
},
config,
data,
} = options;
const { email, password } = data;
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) {
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;
if (!authResult.user) {
throw new AuthenticationError();
}
};
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;

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 { Forbidden } = require('../../errors');
const refresh = async (args) => {
try {
// Await validation here
async function refresh(args) {
// 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') {
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;
if (typeof beforeRefresh === 'function') {
options = await beforeRefresh(options);
}
};
// /////////////////////////////////////
// 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;

View File

@@ -1,93 +1,106 @@
const passport = require('passport');
const executePolicy = require('../executePolicy');
const performFieldOperations = require('../../fields/performFieldOperations');
const executeAccess = require('../executeAccess');
const register = async (args) => {
try {
// /////////////////////////////////////
// 1. Retrieve and execute policy
// /////////////////////////////////////
async function register(args) {
const {
depth,
overrideAccess,
collection: {
Model,
config: collectionConfig,
},
req,
req: {
locale,
fallbackLocale,
},
} = args;
if (!args.overridePolicy) {
await executePolicy(args, args.collection.config.policies.create);
}
let { data } = args;
let options = { ...args };
// /////////////////////////////////////
// 1. Retrieve and execute access
// /////////////////////////////////////
// /////////////////////////////////////
// 2. Execute before register hook
// /////////////////////////////////////
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;
if (!overrideAccess) {
await executeAccess({ req }, collectionConfig.access.create);
}
};
// /////////////////////////////////////
// 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;

View File

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

View File

@@ -1,94 +1,91 @@
const jwt = require('jsonwebtoken');
const { APIError } = require('../../errors');
const resetPassword = async (args) => {
try {
if (!Object.prototype.hasOwnProperty.call(args.data, 'token')
|| !Object.prototype.hasOwnProperty.call(args.data, 'password')) {
throw new APIError('Missing required data.');
}
async function resetPassword(args) {
const { config } = this;
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,
},
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;
if (!Object.prototype.hasOwnProperty.call(args.data, 'token')
|| !Object.prototype.hasOwnProperty.call(args.data, 'password')) {
throw new APIError('Missing required data.');
}
};
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;

View File

@@ -1,123 +1,142 @@
const deepmerge = require('deepmerge');
const overwriteMerge = require('../../utilities/overwriteMerge');
const { NotFound, Forbidden } = require('../../errors');
const executePolicy = require('../executePolicy');
const performFieldOperations = require('../../fields/performFieldOperations');
const executeAccess = require('../executeAccess');
const update = async (args) => {
try {
// /////////////////////////////////////
// 1. Execute policy
// /////////////////////////////////////
async function update(args) {
const { config } = this;
const policyResults = await executePolicy(args, args.config.policies.update);
const hasWherePolicy = typeof policyResults === 'object';
let options = { ...args };
// /////////////////////////////////////
// 2. Retrieve document
// /////////////////////////////////////
const {
const {
depth,
collection: {
Model,
id,
req: {
locale,
fallbackLocale,
},
} = options;
config: collectionConfig,
},
id,
req,
req: {
locale,
fallbackLocale,
},
} = args;
let query = { _id: id };
// /////////////////////////////////////
// 1. Execute access
// /////////////////////////////////////
if (hasWherePolicy) {
query = {
...query,
...policyResults,
};
}
const accessResults = await executeAccess({ req }, collectionConfig.access.update);
const hasWhereAccess = typeof accessResults === 'object';
let user = await Model.findOne(query);
// /////////////////////////////////////
// 2. Retrieve document
// /////////////////////////////////////
if (!user && !hasWherePolicy) throw new NotFound();
if (!user && hasWherePolicy) throw new Forbidden();
let query = { _id: id };
if (locale && user.setLocale) {
user.setLocale(locale, fallbackLocale);
}
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;
if (hasWhereAccess) {
query = {
...query,
...accessResults,
};
}
};
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;

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

View File

@@ -1,13 +1,11 @@
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 {
const token = await login({
const token = await this.operations.collections.auth.login({
req,
res,
collection: req.collection,
config,
data: req.body,
});
@@ -17,8 +15,8 @@ const loginHandler = config => async (req, res) => {
token,
});
} catch (error) {
return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error));
return next(error);
}
};
}
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) => {
return res.status(200).json(req.user);
};
async function me(req, res, next) {
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 formatErrorResponse = require('../../express/responses/formatError');
const { refresh } = require('../operations');
const getExtractJWT = require('../getExtractJWT');
const refreshHandler = config => async (req, res) => {
async function refreshHandler(req, res, next) {
try {
const refreshedToken = await refresh({
const extractJWT = getExtractJWT(this.config);
const token = extractJWT(req);
const result = await this.operations.collections.auth.refresh({
req,
res,
collection: req.collection,
config,
authorization: req.headers.authorization,
token,
});
return res.status(200).json({
message: 'Token refresh successful',
refreshedToken,
...result,
});
} catch (error) {
return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error));
return next(error);
}
};
}
module.exports = refreshHandler;

View File

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

View File

@@ -1,15 +1,12 @@
const httpStatus = require('http-status');
const formatErrorResponse = require('../../express/responses/formatError');
const formatSuccessResponse = require('../../express/responses/formatSuccess');
const { update } = require('../operations');
const updateHandler = async (req, res) => {
async function update(req, res, next) {
try {
const user = await update({
const user = await this.operations.collections.auth.update({
req,
data: req.body,
Model: req.collection.Model,
config: req.collection.config,
collection: req.collection,
id: req.params.id,
});
@@ -18,8 +15,8 @@ const updateHandler = async (req, res) => {
doc: user,
});
} 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;
module.exports = ({ Model, config }) => {
module.exports = ({ operations }, { Model, config }) => {
const opts = {
header: 'Authorization',
prefix: `${config.labels.singular} API-Key `,
};
return new PassportAPIKey(opts, false, (apiKey, done) => {
Model.findOne({ apiKey, enableAPIKey: true }, (err, user) => {
if (err) return done(err);
if (!user) return done(null, false);
return new PassportAPIKey(opts, true, async (req, apiKey, done) => {
try {
const userQuery = await operations.collections.find({
where: {
apiKey: {
equals: apiKey,
},
},
collection: {
Model,
config,
},
req,
overrideAccess: true,
});
const json = user.toJSON({ virtuals: true });
json.collection = config.slug;
return done(null, json);
});
if (userQuery.docs && userQuery.docs.length > 0) {
const user = userQuery.docs[0];
user.collection = config.slug;
done(null, user);
} else {
done(null, false);
}
} catch (err) {
done(null, false);
}
});
};

View File

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

View File

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

View File

@@ -1,24 +1,9 @@
import Cookies from 'universal-cookie';
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 = {
get: (url, params) => {
const query = qs.stringify(params, { addQueryPrefix: true, depth: 10 });
return fetch(`${url}${query}`, {
headers: {
...getJWTHeader(),
},
});
return fetch(`${url}${query}`);
},
post: (url, options = {}) => {
@@ -29,7 +14,6 @@ export const requests = {
method: 'post',
headers: {
...headers,
...getJWTHeader(),
},
};
@@ -44,7 +28,6 @@ export const requests = {
method: 'put',
headers: {
...headers,
...getJWTHeader(),
},
};
@@ -58,7 +41,6 @@ export const requests = {
method: 'delete',
headers: {
...headers,
...getJWTHeader(),
},
});
},

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import {
Route, Switch, withRouter, Redirect,
Route, Switch, withRouter, Redirect, useHistory,
} from 'react-router-dom';
import config from 'payload/config';
import List from './views/collections/List';
@@ -25,17 +25,22 @@ const {
} = config;
const Routes = () => {
const history = useHistory();
const [initialized, setInitialized] = useState(null);
const { user, permissions, permissions: { canAccessAdmin } } = useUser();
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) {
setInitialized(data.initialized);
}
}));
}, []);
useEffect(() => {
history.replace();
}, [history]);
return (
<Route
path={routes.admin}
@@ -62,6 +67,9 @@ const Routes = () => {
<Route path={`${match.url}/logout`}>
<Logout />
</Route>
<Route path={`${match.url}/logout-inactivity`}>
<Logout inactivity />
</Route>
<Route path={`${match.url}/forgot`}>
<ForgotPassword />
</Route>
@@ -94,14 +102,12 @@ const Routes = () => {
key={`${collection.slug}-list`}
path={`${match.url}/collections/${collection.slug}`}
exact
render={(routeProps) => {
return (
<List
{...routeProps}
collection={collection}
/>
);
}}
render={(routeProps) => (
<List
{...routeProps}
collection={collection}
/>
)}
/>
);
}
@@ -116,14 +122,12 @@ const Routes = () => {
key={`${collection.slug}-create`}
path={`${match.url}/collections/${collection.slug}/create`}
exact
render={(routeProps) => {
return (
<Edit
{...routeProps}
collection={collection}
/>
);
}}
render={(routeProps) => (
<Edit
{...routeProps}
collection={collection}
/>
)}
/>
);
}
@@ -138,15 +142,13 @@ const Routes = () => {
key={`${collection.slug}-edit`}
path={`${match.url}/collections/${collection.slug}/:id`}
exact
render={(routeProps) => {
return (
<Edit
isEditing
{...routeProps}
collection={collection}
/>
);
}}
render={(routeProps) => (
<Edit
isEditing
{...routeProps}
collection={collection}
/>
)}
/>
);
}
@@ -161,14 +163,12 @@ const Routes = () => {
key={`${global.slug}`}
path={`${match.url}/globals/${global.slug}`}
exact
render={(routeProps) => {
return (
<EditGlobal
{...routeProps}
global={global}
/>
);
}}
render={(routeProps) => (
<EditGlobal
{...routeProps}
global={global}
/>
)}
/>
);
}
@@ -189,6 +189,10 @@ const Routes = () => {
return <Loading />;
}
if (user === undefined) {
return <Loading />;
}
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 = {
...(field.components || {}),
...(field.admin.components || {}),
};
if (field.fields) {
@@ -56,7 +56,7 @@ function customComponents(config) {
newComponents[collection.slug] = {
fields: recursivelyAddFieldComponents(collection.fields),
...(collection.components || {}),
...(collection.admin.components || {}),
};
return newComponents;
@@ -67,7 +67,7 @@ function customComponents(config) {
newComponents[global.slug] = {
fields: recursivelyAddFieldComponents(global.fields),
...(global.components || {}),
...(global.admin.components || {}),
};
return newComponents;
@@ -76,7 +76,7 @@ function customComponents(config) {
const string = stringify({
...(allCollectionComponents || {}),
...(allGlobalComponents || {}),
...(config.components || {}),
...(config.admin.components || {}),
}).replace(/\\/g, '\\\\');
return {

View File

@@ -1,18 +1,16 @@
import React, {
useState, createContext, useContext, useEffect, useCallback,
} from 'react';
import { useLocation, useHistory } from 'react-router-dom';
import jwt from 'jsonwebtoken';
import { useLocation, useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import Cookies from 'universal-cookie';
import config from 'payload/config';
import { useModal } from '@trbl/react-modal';
import { useModal } from '@faceless-ui/modal';
import { requests } from '../../api';
import StayLoggedInModal from '../modals/StayLoggedIn';
import useDebounce from '../../hooks/useDebounce';
const {
cookiePrefix,
admin: {
user: userSlug,
},
@@ -23,16 +21,12 @@ const {
},
} = config;
const cookieTokenName = `${cookiePrefix}-token`;
const cookies = new Cookies();
const Context = createContext({});
const isNotExpired = decodedJWT => (decodedJWT?.exp || 0) > Date.now() / 1000;
const UserProvider = ({ children }) => {
const [token, setToken] = useState('');
const [user, setUser] = useState(null);
const [user, setUser] = useState(undefined);
const [tokenInMemory, setTokenInMemory] = useState(null);
const exp = user?.exp;
const [permissions, setPermissions] = useState({ canAccessAdmin: null });
@@ -42,64 +36,72 @@ const UserProvider = ({ children }) => {
const [lastLocationChange, setLastLocationChange] = useState(0);
const debouncedLocationChange = useDebounce(lastLocationChange, 10000);
const exp = user?.exp || 0;
const email = user?.email;
const refreshToken = useCallback(() => {
// Need to retrieve token straight from cookie so as to keep this function
// with no dependencies and to make sure we have the exact token that will be used
// in the request to the /refresh route
const tokenFromCookie = cookies.get(cookieTokenName);
const decodedToken = jwt.decode(tokenFromCookie);
const refreshCookie = useCallback(() => {
const now = Math.round((new Date()).getTime() / 1000);
const remainingTime = (exp || 0) - now;
if (decodedToken?.exp > (Date.now() / 1000)) {
if (exp && remainingTime < 120) {
setTimeout(async () => {
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`);
if (request.status === 200) {
const json = await request.json();
setToken(json.refreshedToken);
setUser(json.user);
} else {
history.push(`${admin}/logout-inactivity`);
}
}, 1000);
}
}, [setToken]);
}, [setUser, history, exp]);
const setToken = useCallback((token) => {
const decoded = jwt.decode(token);
setUser(decoded);
setTokenInMemory(token);
}, []);
const logOut = () => {
setUser(null);
setToken(null);
cookies.remove(cookieTokenName, { path: '/' });
setTokenInMemory(null);
requests.get(`${serverURL}${api}/${userSlug}/logout`);
};
// On mount, get cookie and set as token
// On mount, get user and set
useEffect(() => {
const cookieToken = cookies.get(cookieTokenName);
if (cookieToken) setToken(cookieToken);
}, []);
const fetchMe = async () => {
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(() => {
refreshToken();
}, [debouncedLocationChange, refreshToken]);
if (email) {
refreshCookie();
}
}, [debouncedLocationChange, refreshCookie, email]);
useEffect(() => {
setLastLocationChange(Date.now());
}, [pathname]);
// When token changes, set cookie, decode and set user
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
// When user changes, get new access
useEffect(() => {
async function getPermissions() {
const request = await requests.get(`${serverURL}${api}/policies`);
const request = await requests.get(`${serverURL}${api}/access`);
if (request.status === 200) {
const json = await request.json();
@@ -135,7 +137,6 @@ const UserProvider = ({ children }) => {
if (remainingTime > 0) {
forceLogOut = setTimeout(() => {
logOut();
history.push(`${admin}/logout`);
closeAllModals();
}, remainingTime * 1000);
@@ -149,15 +150,15 @@ const UserProvider = ({ children }) => {
return (
<Context.Provider value={{
user,
setToken,
logOut,
refreshToken,
token,
refreshCookie,
permissions,
setToken,
token: tokenInMemory,
}}
>
{children}
<StayLoggedInModal refreshToken={refreshToken} />
<StayLoggedInModal refreshCookie={refreshCookie} />
</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) => {
let initialColumns = [];
const hasThumbnail = fields.find(field => field.type === 'thumbnail');
const hasThumbnail = fields.find((field) => field.type === 'thumbnail');
if (Array.isArray(defaultColumns)) {
initialColumns = defaultColumns;
if (Array.isArray(defaultColumns) && defaultColumns.length >= 1) {
return {
columns: defaultColumns,
};
}
if (hasThumbnail) {
@@ -15,11 +17,8 @@ const getInitialColumnState = (fields, useAsTitle, defaultColumns) => {
initialColumns.push(useAsTitle);
}
const remainingColumns = fields.filter((field) => {
return field.name !== useAsTitle && field.type !== 'thumbnail';
}).slice(0, 3 - initialColumns.length).map((field) => {
return field.name;
});
const remainingColumns = fields.filter((field) => field.name !== useAsTitle && field.type !== 'thumbnail')
.slice(0, 3 - initialColumns.length).map((field) => field.name);
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 {
collection: {
fields,
useAsTitle,
admin: {
useAsTitle,
defaultColumns,
},
},
handleChange,
defaultColumns,
} = props;
const [initialColumns, setInitialColumns] = useState([]);
@@ -55,7 +57,7 @@ const ColumnSelector = (props) => {
return (
<div className={baseClass}>
{fields && fields.map((field, i) => {
const isEnabled = columns.find(column => column === field.name);
const isEnabled = columns.find((column) => column === field.name);
return (
<Pill
onClick={() => dispatchColumns({ payload: field.name, type: isEnabled ? 'disable' : 'enable' })}
@@ -73,20 +75,18 @@ const ColumnSelector = (props) => {
);
};
ColumnSelector.defaultProps = {
defaultColumns: undefined,
};
ColumnSelector.propTypes = {
collection: PropTypes.shape({
fields: PropTypes.arrayOf(
PropTypes.shape({}),
),
useAsTitle: PropTypes.string,
admin: PropTypes.shape({
defaultColumns: PropTypes.arrayOf(
PropTypes.string,
),
useAsTitle: PropTypes.string,
}),
}).isRequired,
defaultColumns: PropTypes.arrayOf(
PropTypes.string,
),
handleChange: PropTypes.func.isRequired,
};

View File

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

View File

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

View File

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

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