From b6231925bcea5116afcf366c7247146a5c8899d2 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 19 Jan 2020 15:04:31 -0500 Subject: [PATCH] scaffolds RenderFields, moves Init into Routes --- demo/collections/Page/index.js | 19 +- demo/collections/User.js | 12 +- demo/policies/checkRole.js | 16 ++ demo/policies/roles.js | 7 + src/auth/checkRoleMiddleware.js | 23 --- src/auth/loadPolicy.js | 10 +- src/auth/requestHandlers.js | 8 +- src/auth/routes.js | 7 - src/client/api.js | 6 +- src/client/components/Routes.js | 187 ++++++++++-------- .../components/forms/RenderFields/index.js | 21 ++ src/client/components/forms/Submit/index.js | 40 ++-- .../forms/field-types/Group/index.js | 24 ++- .../forms/field-types/Input/index.js | 2 +- .../forms/field-types/Media/index.js | 3 +- .../forms/field-types/Repeater/index.js | 2 +- .../forms/field-types/Textarea/index.js | 74 +++++-- .../components/forms/field-types/index.js | 8 + src/client/components/index.js | 21 +- .../components/modules/UploadMedia/index.js | 4 +- src/client/components/utilities/Init/index.js | 26 --- .../components/views/CreateFirstUser/index.js | 38 ++++ .../views/CreateFirstUser/index.scss | 16 ++ src/client/components/views/Login/index.js | 6 +- src/client/components/views/Login/index.scss | 3 +- src/index.js | 2 +- 26 files changed, 350 insertions(+), 235 deletions(-) create mode 100644 demo/policies/checkRole.js create mode 100644 demo/policies/roles.js delete mode 100644 src/auth/checkRoleMiddleware.js create mode 100644 src/client/components/forms/RenderFields/index.js create mode 100644 src/client/components/forms/field-types/index.js delete mode 100644 src/client/components/utilities/Init/index.js create mode 100644 src/client/components/views/CreateFirstUser/index.js create mode 100644 src/client/components/views/CreateFirstUser/index.scss diff --git a/demo/collections/Page/index.js b/demo/collections/Page/index.js index 2957dbda9a..a81d365b89 100644 --- a/demo/collections/Page/index.js +++ b/demo/collections/Page/index.js @@ -1,5 +1,4 @@ -const HttpStatus = require('http-status'); -const checkRoleMiddleware = require('../../../src/auth/checkRoleMiddleware'); +const checkRole = require('../../policies/checkRole'); const Quote = require('../../content-blocks/Quote'); const CallToAction = require('../../content-blocks/CallToAction'); const List = require('./components/List'); @@ -16,19 +15,9 @@ module.exports = { // null or undefined policies will default to requiring auth // any policy can use req.user to see that the user is logged create: null, - read: (req, res, next) => { - // allow anonymous access - next(); - }, - update: checkRoleMiddleware('user', 'admin'), - destroy: (req, res, next) => { - if (req.user && req.user.role) { - next(); - return; - } - res.status(HttpStatus.FORBIDDEN) - .send(); - }, + read: () => true, + update: user => checkRole(['user', 'admin'], user), + destroy: user => checkRole(['user', 'admin'], user), }, fields: [ { diff --git a/demo/collections/User.js b/demo/collections/User.js index da864ce509..ef41822127 100644 --- a/demo/collections/User.js +++ b/demo/collections/User.js @@ -1,4 +1,4 @@ -const payloadConfig = require('../payload.config'); +const roles = require('../policies/roles'); module.exports = { slug: 'users', @@ -7,6 +7,7 @@ module.exports = { plural: 'Users', }, useAsTitle: 'email', + useAsUsername: 'email', policies: { create: (req, res, next) => { return next(); @@ -21,13 +22,6 @@ module.exports = { return next(); }, }, - roles: [ - 'admin', - 'editor', - 'moderator', - 'user', - 'viewer', - ], auth: { strategy: 'jwt', passwordResets: true, @@ -45,7 +39,7 @@ module.exports = { { name: 'role', type: 'enum', - enum: payloadConfig.roles, + enum: roles, default: 'user', }, ], diff --git a/demo/policies/checkRole.js b/demo/policies/checkRole.js new file mode 100644 index 0000000000..56150227c4 --- /dev/null +++ b/demo/policies/checkRole.js @@ -0,0 +1,16 @@ +/** + * authorize a request by comparing the current user with one or more roles + * @param roles + * @param user + * @returns {Function} + */ + +const checkRole = (roles, user) => { + if (user && roles.some(role => role === user.role)) { + return true; + } + + return false; +}; + +module.exports = checkRole; diff --git a/demo/policies/roles.js b/demo/policies/roles.js new file mode 100644 index 0000000000..40a0fa8455 --- /dev/null +++ b/demo/policies/roles.js @@ -0,0 +1,7 @@ +module.exports = [ + 'admin', + 'editor', + 'moderator', + 'user', + 'viewer', +]; diff --git a/src/auth/checkRoleMiddleware.js b/src/auth/checkRoleMiddleware.js deleted file mode 100644 index 4e93db433e..0000000000 --- a/src/auth/checkRoleMiddleware.js +++ /dev/null @@ -1,23 +0,0 @@ -const HttpStatus = require('http-status'); - -/** - * authorize a request by comparing the current user with one or more roles - * @param roles - * @returns {Function} - */ - -const checkRoleMiddleware = (...roles) => { - return (req, res, next) => { - if (!req.user) { - res.status(HttpStatus.UNAUTHORIZED) - .send('Not Authorized'); - } else if (!roles.some(role => role === req.user.role)) { - res.status(HttpStatus.FORBIDDEN) - .send('Role not authorized.'); - } else { - next(); - } - }; -}; - -module.exports = checkRoleMiddleware; diff --git a/src/auth/loadPolicy.js b/src/auth/loadPolicy.js index f49d6fcac0..5df94cdeb8 100644 --- a/src/auth/loadPolicy.js +++ b/src/auth/loadPolicy.js @@ -13,9 +13,13 @@ export const loadPolicy = (policy) => { passport.authenticate(['jwt', 'anonymous'], { session: false }), (req, res, next) => { if (policy) { - policy(req, res, next); - } else { - requireAuth(req, res); + if (!policy(req.user)) { + return res.status(HttpStatus.FORBIDDEN) + .send('Role not authorized.'); + } + + return next(); } + requireAuth(req, res); }]; }; diff --git a/src/auth/requestHandlers.js b/src/auth/requestHandlers.js index c91057aef7..862280f224 100644 --- a/src/auth/requestHandlers.js +++ b/src/auth/requestHandlers.js @@ -17,8 +17,8 @@ export default User => ({ const error = new APIError('Authentication error', httpStatus.UNAUTHORIZED); return next(error); } - passport.authenticate('local')(req, res, () => { - res.json({ email: user.email, role: user.role, createdAt: user.createdAt }); + return passport.authenticate('local')(req, res, () => { + return res.json({ email: user.email, role: user.role, createdAt: user.createdAt }); }); }); }, @@ -35,7 +35,7 @@ export default User => ({ User.findByUsername(email, (err, user) => { if (err || !user) return res.status(401).json({ message: 'Auth Failed' }); - user.authenticate(password, (authErr, model, passwordError) => { + return user.authenticate(password, (authErr, model, passwordError) => { if (authErr || passwordError) return res.status(401).json({ message: 'Auth Failed' }); const opts = {}; @@ -73,6 +73,6 @@ export default User => ({ next(error); } - next(); + return next(); }, }); diff --git a/src/auth/routes.js b/src/auth/routes.js index 9c3252f720..1839838256 100644 --- a/src/auth/routes.js +++ b/src/auth/routes.js @@ -2,7 +2,6 @@ import express from 'express'; import passport from 'passport'; import authRequestHandlers from './requestHandlers'; import authValidate from './validate'; -import checkRoleMiddleware from './checkRoleMiddleware'; import passwordResetRoutes from './passwordResets/routes'; const router = express.Router(); @@ -17,12 +16,6 @@ const authRoutes = (userConfig, User) => { .route('/me') .post(passport.authenticate(userConfig.auth.strategy, { session: false }), auth.me); - userConfig.roles.forEach((role) => { - router - .route(`/role/${role}`) - .get(passport.authenticate(userConfig.auth.strategy, { session: false }), checkRoleMiddleware(role), auth.me); - }); - if (userConfig.auth.passwordResets) { router.use('', passwordResetRoutes(userConfig.email, User)); } diff --git a/src/client/api.js b/src/client/api.js index fa16eb7d6a..52f0b95a0e 100644 --- a/src/client/api.js +++ b/src/client/api.js @@ -15,7 +15,7 @@ const requests = { headers: { ...setJWT() } - }).then(res => res.body); + }); }, post: (url, body) => @@ -25,7 +25,7 @@ const requests = { headers: { ...setJWT() }, - }).then(res => res.body), + }), put: (url, body) => fetch(`${url}`, { @@ -34,7 +34,7 @@ const requests = { headers: { ...setJWT() }, - }).then(res => res.body), + }), }; export default { diff --git a/src/client/components/Routes.js b/src/client/components/Routes.js index a9606098e0..088c48cd66 100644 --- a/src/client/components/Routes.js +++ b/src/client/components/Routes.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import Cookies from 'universal-cookie'; import { Route, Switch, withRouter, Redirect, @@ -7,102 +7,127 @@ import config from 'payload-config'; import DefaultTemplate from './layout/DefaultTemplate'; import Dashboard from './views/Dashboard'; import Login from './views/Login'; +import CreateFirstUser from './views/CreateFirstUser'; import CreateUser from './views/CreateUser'; import MediaLibrary from './views/MediaLibrary'; import Edit from './views/collections/Edit'; import List from './views/collections/List'; +import { requests } from '../api'; const cookies = new Cookies(); const Routes = () => { + const [initialized, setInitialized] = useState(null); + + useEffect(() => { + requests.get('/init').then(res => res.json().then((data) => { + if (data && 'initialized' in data) { + setInitialized(data.initialized); + } + })); + }, []); + return ( { - return ( - - } - /> - { return

Forgot Password

; }} - /> - { - if (cookies.get('token')) { - return ( - - - - + if (initialized === false) { + return ( + + + + + + + + + ); + } if (initialized === true) { + return ( + + + + + +

Forgot Password

+
+ { + if (cookies.get('token')) { + return ( + + + + - {config.collections.map((collection) => { - const components = collection.components ? collection.components : {}; - return ( - - { - return ( - - ); - }} - /> + {config.collections.map((collection) => { + const components = collection.components ? collection.components : {}; + return ( + + { + return ( + + ); + }} + /> - { - return ( - - ); - }} - /> + { + return ( + + ); + }} + /> - { - const ListComponent = components.List ? components.List : List; - return ( - - ); - }} - /> + { + const ListComponent = components.List ? components.List : List; + return ( + + ); + }} + /> - - ); - })} - - ); - } - return ; - }} - /> -
- ); +
+ ); + })} + + ); + } + return ; + }} + /> + + ); + } + + return null; }} /> ); diff --git a/src/client/components/forms/RenderFields/index.js b/src/client/components/forms/RenderFields/index.js new file mode 100644 index 0000000000..de72f35137 --- /dev/null +++ b/src/client/components/forms/RenderFields/index.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import fieldTypes from '../field-types'; + +const RenderFields = ({ fields }) => { + if (fields) { + return fields.map((field, i) => { + return field.name + i; + }); + } + + return null; +}; + +RenderFields.propTypes = { + fields: PropTypes.arrayOf( + PropTypes.shape({}), + ).isRequired, +}; + +export default RenderFields; diff --git a/src/client/components/forms/Submit/index.js b/src/client/components/forms/Submit/index.js index 97e2d35e3e..b91758acbb 100644 --- a/src/client/components/forms/Submit/index.js +++ b/src/client/components/forms/Submit/index.js @@ -1,32 +1,28 @@ -import React, { Component } from 'react'; +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; import FormContext from '../Form/Context'; import Button from '../../controls/Button'; import './index.scss'; -class FormSubmit extends Component { - render() { - return ( -
- -
- ); - } -} +const baseClass = 'form-submit'; -const ContextFormSubmit = (props) => { +const FormSubmit = ({ children }) => { + const formContext = useContext(FormContext); return ( - - {context => ( - - )} - +
+ +
); }; -export default ContextFormSubmit; +FormSubmit.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]).isRequired, +}; + +export default FormSubmit; diff --git a/src/client/components/forms/field-types/Group/index.js b/src/client/components/forms/field-types/Group/index.js index 7c2be3070f..ef1164207d 100644 --- a/src/client/components/forms/field-types/Group/index.js +++ b/src/client/components/forms/field-types/Group/index.js @@ -1,12 +1,28 @@ import React from 'react'; -import { Section } from 'payload/components'; +import PropTypes from 'prop-types'; +import Section from '../../../layout/Section'; -const Group = props => { +const Group = ({ label, children }) => { return ( -
- {props.children} +
+ {children}
); }; +Group.defaultProps = { + label: '', +}; + +Group.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]).isRequired, + label: PropTypes.string, +}; + export default Group; diff --git a/src/client/components/forms/field-types/Input/index.js b/src/client/components/forms/field-types/Input/index.js index 206868b90f..3ac82a58a5 100644 --- a/src/client/components/forms/field-types/Input/index.js +++ b/src/client/components/forms/field-types/Input/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import { fieldType } from 'payload/components'; +import fieldType from '../fieldType'; import './index.scss'; diff --git a/src/client/components/forms/field-types/Media/index.js b/src/client/components/forms/field-types/Media/index.js index 902c704b8c..6b4a3679ab 100644 --- a/src/client/components/forms/field-types/Media/index.js +++ b/src/client/components/forms/field-types/Media/index.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; -import { fieldType, UploadMedia } from 'payload/components'; +import fieldType from '../fieldType'; +import UploadMedia from '../../../modules/UploadMedia'; import './index.scss'; diff --git a/src/client/components/forms/field-types/Repeater/index.js b/src/client/components/forms/field-types/Repeater/index.js index b9bb87aefa..5a8c7a3215 100644 --- a/src/client/components/forms/field-types/Repeater/index.js +++ b/src/client/components/forms/field-types/Repeater/index.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { Section } from 'payload/components'; +import Section from '../../../layout/Section'; class Repeater extends Component { constructor(props) { diff --git a/src/client/components/forms/field-types/Textarea/index.js b/src/client/components/forms/field-types/Textarea/index.js index 24bd088f17..a991ec3cbc 100644 --- a/src/client/components/forms/field-types/Textarea/index.js +++ b/src/client/components/forms/field-types/Textarea/index.js @@ -1,28 +1,70 @@ import React from 'react'; -import { fieldType } from 'payload/components'; +import PropTypes from 'prop-types'; +import fieldType from '../fieldType'; import './index.scss'; -const error = 'Please fill in the textarea'; +const errorMessage = 'Please fill in the textarea'; const validate = value => value.length > 0; -const Textarea = props => { +const Textarea = (props) => { + const { + className, + style, + error, + label, + value, + onChange, + disabled, + placeholder, + id, + name, + } = props; + return ( -
- {props.error} - {props.label} +
+ {error} + {label} + value={value || ''} + onChange={onChange} + disabled={disabled} + placeholder={placeholder} + id={id || name} + name={name} + />
); -} +}; -export default fieldType(Textarea, 'textarea', validate, error); +Textarea.defaultProps = { + className: null, + style: {}, + error: null, + label: null, + value: '', + onChange: null, + disabled: null, + placeholder: null, + id: null, + name: 'textarea', +}; + +Textarea.propTypes = { + className: PropTypes.string, + style: PropTypes.shape({}), + error: PropTypes.node, + label: PropTypes.string, + value: PropTypes.string, + onChange: PropTypes.func, + disabled: PropTypes.string, + placeholder: PropTypes.string, + id: PropTypes.string, + name: PropTypes.string, +}; + +export default fieldType(Textarea, 'textarea', validate, errorMessage); diff --git a/src/client/components/forms/field-types/index.js b/src/client/components/forms/field-types/index.js new file mode 100644 index 0000000000..984427ddf9 --- /dev/null +++ b/src/client/components/forms/field-types/index.js @@ -0,0 +1,8 @@ +export { default as Email } from './Email'; +export { default as Group } from './Group'; +export { default as HiddenInput } from './HiddenInput'; +export { default as Input } from './Input'; +export { default as Media } from './Media'; +export { default as Password } from './Password'; +export { default as Repeater } from './Repeater'; +export { default as Textarea } from './Textarea'; diff --git a/src/client/components/index.js b/src/client/components/index.js index 872da6be1f..1eabec662f 100644 --- a/src/client/components/index.js +++ b/src/client/components/index.js @@ -1,7 +1,6 @@ import React from 'react'; import { render } from 'react-dom'; import { BrowserRouter as Router } from 'react-router-dom'; -import Init from './utilities/Init'; import { SearchParamsProvider } from './utilities/SearchParams'; import { LocaleProvider } from './utilities/Locale'; import { StatusListProvider } from './modules/Status'; @@ -11,17 +10,15 @@ import '../scss/app.scss'; const Index = () => { return ( - - - - - - - - - - - + + + + + + + + + ); }; diff --git a/src/client/components/modules/UploadMedia/index.js b/src/client/components/modules/UploadMedia/index.js index 13dcb0c78c..d2beebdac6 100644 --- a/src/client/components/modules/UploadMedia/index.js +++ b/src/client/components/modules/UploadMedia/index.js @@ -1,8 +1,8 @@ import React, { Component } from 'react'; import { createPortal } from 'react-dom'; import { connect } from 'react-redux'; -import { Button } from 'payload/components'; -import api from 'payload/api'; +import Button from '../../controls/Button'; +import api from '../../../api'; import './index.scss'; diff --git a/src/client/components/utilities/Init/index.js b/src/client/components/utilities/Init/index.js deleted file mode 100644 index 8a62df80f7..0000000000 --- a/src/client/components/utilities/Init/index.js +++ /dev/null @@ -1,26 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; - -const Init = ({ children }) => { - const [initialized, setInitialized] = useState(false); - - if (initialized) { - return ( - { children } - ); - } - - return ( -

Not initialized

- ); -}; - -Init.propTypes = { - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - ]).isRequired, -}; - - -export default Init; diff --git a/src/client/components/views/CreateFirstUser/index.js b/src/client/components/views/CreateFirstUser/index.js new file mode 100644 index 0000000000..60c4893fae --- /dev/null +++ b/src/client/components/views/CreateFirstUser/index.js @@ -0,0 +1,38 @@ +import React from 'react'; +import Cookies from 'universal-cookie'; +import config from 'payload-config'; +import ContentBlock from '../../layout/ContentBlock'; +import Form from '../../forms/Form'; +import RenderFields from '../../forms/RenderFields'; +import FormSubmit from '../../forms/Submit'; + +import './index.scss'; + +const cookies = new Cookies(); + +const handleAjaxResponse = (res) => { + cookies.set('token', res.token, { path: '/' }); +}; + +const baseClass = 'create-first-user'; + +const CreateFirstUser = () => { + return ( + +
+

Welcome to Payload

+

To begin, create your first user.

+
+ + Create + +
+
+ ); +}; + +export default CreateFirstUser; diff --git a/src/client/components/views/CreateFirstUser/index.scss b/src/client/components/views/CreateFirstUser/index.scss new file mode 100644 index 0000000000..be6ea1adee --- /dev/null +++ b/src/client/components/views/CreateFirstUser/index.scss @@ -0,0 +1,16 @@ +@import '../../../scss/styles'; + +.create-first-user { + display: flex; + align-items: center; + flex-wrap: wrap; + min-height: 100vh; + + &__wrap { + margin: 0 auto base(1); + + svg { + width: 100%; + } + } +} diff --git a/src/client/components/views/Login/index.js b/src/client/components/views/Login/index.js index 258b35b735..68970591a8 100644 --- a/src/client/components/views/Login/index.js +++ b/src/client/components/views/Login/index.js @@ -15,13 +15,15 @@ const handleAjaxResponse = (res) => { cookies.set('token', res.token, { path: '/' }); }; +const baseClass = 'login'; + const Login = () => { return ( -
+
{ this.config.user.fields.push(...baseUserFields); const userSchema = buildCollectionSchema(this.config.user, this.config); - userSchema.plugin(passportLocalMongoose, { usernameField: 'email' }); + userSchema.plugin(passportLocalMongoose, { usernameField: this.config.user.useAsUsername }); this.User = mongoose.model(this.config.user.labels.singular, userSchema); initUserAuth(this.User, this.config, this.router);