merges with master
This commit is contained in:
67
.eslintrc.js
67
.eslintrc.js
@@ -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"]
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,8 +9,8 @@ module.exports = {
|
||||
{
|
||||
name: 'author',
|
||||
label: 'Author',
|
||||
type: 'text',
|
||||
maxLength: 100,
|
||||
type: 'relationship',
|
||||
relationTo: 'public-users',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
26
demo/collections/Blocks.js
Normal file
26
demo/collections/Blocks.js
Normal 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
const NestedArrayCustomField = () => <div className="nested-array-custom-field">Nested array custom field</div>;
|
||||
|
||||
export default NestedArrayCustomField;
|
||||
@@ -1,5 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const NestedRepeaterCustomField = () => <div className="nested-repeater-custom-field">Nested repeater custom field</div>;
|
||||
|
||||
export default NestedRepeaterCustomField;
|
||||
@@ -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'),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
|
||||
50
demo/collections/LocalizedArray.js
Normal file
50
demo/collections/LocalizedArray.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
slug: 'relationship-a',
|
||||
policies: {
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
labels: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
slug: 'relationship-b',
|
||||
policies: {
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
labels: {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
21
demo/globals/BlocksGlobal.js
Normal file
21
demo/globals/BlocksGlobal.js
Normal 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
34
demo/globals/GlobalWithStrictAccess.js
Normal file
34
demo/globals/GlobalWithStrictAccess.js
Normal 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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',
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
5
errors.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { APIError } = require('./src/errors');
|
||||
|
||||
module.exports = {
|
||||
APIError,
|
||||
};
|
||||
2
hooks.js
2
hooks.js
@@ -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';
|
||||
|
||||
90
package.json
90
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -6,6 +6,9 @@ module.exports = [
|
||||
label: 'Email',
|
||||
type: 'email',
|
||||
validate: validations.email,
|
||||
admin: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'resetPasswordToken',
|
||||
|
||||
@@ -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
22
src/auth/executeAccess.js
Normal 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;
|
||||
@@ -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;
|
||||
40
src/auth/getExecuteStaticAccess.js
Normal file
40
src/auth/getExecuteStaticAccess.js
Normal 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;
|
||||
@@ -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
21
src/auth/getExtractJWT.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
18
src/auth/graphql/resolvers/logout.js
Normal file
18
src/auth/graphql/resolvers/logout.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
10
src/auth/init.js
Normal 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;
|
||||
107
src/auth/operations/access.js
Normal file
107
src/auth/operations/access.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
34
src/auth/operations/logout.js
Normal file
34
src/auth/operations/logout.js
Normal 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
31
src/auth/operations/me.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
16
src/auth/requestHandlers/access.js
Normal file
16
src/auth/requestHandlers/access.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
15
src/auth/requestHandlers/logout.js
Normal file
15
src/auth/requestHandlers/logout.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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`} />;
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
49
src/client/components/elements/Card/index.js
Normal file
49
src/client/components/elements/Card/index.js
Normal 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;
|
||||
41
src/client/components/elements/Card/index.scss
Normal file
41
src/client/components/elements/Card/index.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user