From 535fffaf418cc0979930c75f461c4e8199ad1a6d Mon Sep 17 00:00:00 2001 From: John Lyon-Smith Date: Mon, 5 Mar 2018 15:18:08 -0800 Subject: [PATCH] New assets, fixed list box scrolling, header text, etc.. --- server/src/api/routes/AuthRoutes.js | 9 +- server/src/api/routes/UserRoutes.js | 58 +++--------- server/src/bin/addUser.js | 2 +- server/src/database/schemas/user.js | 25 +----- website/package-lock.json | 6 +- website/package.json | 2 +- website/src/App.js | 26 ++++-- website/src/Auth/Login.js | 6 +- website/src/Auth/Logout.js | 3 +- website/src/Auth/ProtectedRoute.js | 13 +-- website/src/Modal/MessageModal.js | 2 +- website/src/Modal/YesNoMessageModal.js | 75 +++++++++++----- website/src/Profile/ProfileForm.js | 3 - website/src/Users/UserForm.js | 55 ++++++------ website/src/Users/UserList.js | 20 ++--- website/src/Users/Users.js | 84 +++++++++--------- website/src/assets/icons/admin.svg | 15 ++++ website/src/assets/icons/confirmed.svg | 17 ++++ website/src/assets/icons/edit.svg | 14 +++ .../src/assets/icons/{hold.svg => hand.svg} | 0 website/src/assets/icons/help.svg | 18 ++++ website/src/assets/icons/users.png | Bin 1665 -> 0 bytes website/src/assets/icons/warning.svg | 18 ++++ website/src/ui/BoundButton.js | 8 +- website/src/ui/BoundEmailIcon.js | 4 +- website/src/ui/BoundInput.js | 1 - website/src/ui/Box.js | 20 +++-- website/src/ui/Button.js | 14 +-- website/src/ui/HeaderText.js | 35 ++++++++ website/src/ui/Icon.js | 7 +- website/src/ui/Input.js | 8 +- website/src/ui/List.js | 16 ++-- website/src/ui/index.js | 1 + 33 files changed, 354 insertions(+), 231 deletions(-) create mode 100644 website/src/assets/icons/admin.svg create mode 100644 website/src/assets/icons/confirmed.svg create mode 100644 website/src/assets/icons/edit.svg rename website/src/assets/icons/{hold.svg => hand.svg} (100%) create mode 100644 website/src/assets/icons/help.svg delete mode 100644 website/src/assets/icons/users.png create mode 100644 website/src/assets/icons/warning.svg create mode 100644 website/src/ui/HeaderText.js diff --git a/server/src/api/routes/AuthRoutes.js b/server/src/api/routes/AuthRoutes.js index dfcd004..1457ce6 100644 --- a/server/src/api/routes/AuthRoutes.js +++ b/server/src/api/routes/AuthRoutes.js @@ -90,13 +90,13 @@ export class AuthRoutes { res.set('Authorization', `Bearer ${savedUser.loginToken}`) res.json(savedUser.toClient()) } else { - return Promise.reject(createError.BadRequest('Email or password incorrect')) + return Promise.reject(createError.BadRequest('email or password incorrect')) } }).catch((err) => { if (err instanceof createError.HttpError) { next(err) } else { - next(createError.InternalServerError(`Unable to login. ${err ? err.message : ''}`)) + next(createError.InternalServerError(`${err ? err.message : ''}`)) } }) } @@ -126,11 +126,10 @@ export class AuthRoutes { let existingEmail = req.body.existingEmail const newEmail = req.body.newEmail let User = this.db.User - const role = req.user.role - const isAdminOrExec = (role === 'executive' || role === 'administrator') + const isAdmin = !!req.user.administrator if (existingEmail) { - if (!isAdminOrExec) { + if (!isAdmin) { return next(createError.Forbidden('Only admins can resend change email to any user')) } } else { diff --git a/server/src/api/routes/UserRoutes.js b/server/src/api/routes/UserRoutes.js index 443441a..d332a43 100644 --- a/server/src/api/routes/UserRoutes.js +++ b/server/src/api/routes/UserRoutes.js @@ -24,9 +24,6 @@ export class UserRoutes { .post(passport.authenticate('bearer', { session: false }), this.createUser) .put(passport.authenticate('bearer', { session: false }), this.updateUser) - app.route('/users/brokers') - .get(passport.authenticate('bearer', { session: false }), this.listBrokerUsers) - app.route('/users/:_id([a-f0-9]{24})') .get(passport.authenticate('bearer', { session: false }), this.getUser) .delete(passport.authenticate('bearer', { session: false }), this.deleteUser) @@ -45,9 +42,9 @@ export class UserRoutes { const User = this.db.User const limit = req.params.limit || 20 const skip = req.params.skip || 0 - const role = req.user.role + const isAdmin = !!req.user.administrator - if (role !== 'executive' && role !== 'administrator') { + if (!isAdmin) { return next(new createError.Forbidden()) } @@ -76,41 +73,14 @@ export class UserRoutes { }) } - listBrokerUsers(req, res, next) { - let User = this.db.User - const role = req.user.role - - if (role !== 'executive' && role !== 'administrator') { - return next(new createError.Forbidden()) - } - - let users = [] - let cursor = User.find({ role: 'broker' }) - .select('_id firstName lastName thumbnailImageId t12 aum numHouseholds cellPhone').cursor() - - cursor.on('data', (doc) => { - users.push(doc) - }) - cursor.on('end', () => { - res.json({ - total: users.length, - offset: 0, - count: users.length, - items: users - }) - }) - cursor.on('error', (err) => { - next(createError.InternalServerError(err.message)) - }) - } - getUser(req, res, next) { let User = this.db.User const _id = req.params._id const isSelf = (_id === req.user._id) + const isAdmin = req.user.administrator // User can see themselves, otherwise must be super user - if (!isSelf && role !== 'executive' && role !== 'administrator') { + if (!isSelf && !isAdmin) { return next(new createError.Forbidden()) } @@ -130,9 +100,9 @@ export class UserRoutes { } createUser(req, res, next) { - const role = req.user.role + const isAdmin = req.user.administrator - if (role !== 'executive' && role !== 'administrator') { + if (!isAdmin) { return next(new createError.Forbidden()) } @@ -168,7 +138,7 @@ export class UserRoutes { } updateUser(req, res, next) { - const role = req.user.role + const isAdmin = req.user.administrator // Do this here because Mongoose will add it automatically otherwise if (!req.body._id) { @@ -178,7 +148,7 @@ export class UserRoutes { const isSelf = (req.body._id === req.user._id.toString()) // User can change themselves, otherwise must be super user - if (!isSelf && role !== 'executive' && role !== 'administrator') { + if (!isSelf && !isAdmin) { return next(new createError.Forbidden()) } @@ -191,8 +161,8 @@ export class UserRoutes { return next(createError.BadRequest('Invalid data')) } - if (isSelf && userUpdates.role && userUpdates.role !== req.user.role) { - return next(createError.BadRequest('Cannot modify own role')) + if (isSelf && !isAdmin) { + return next(createError.BadRequest('Cannot modify own administrator level')) } User.findById(userUpdates._id).then((foundUser) => { @@ -213,7 +183,7 @@ export class UserRoutes { } setImage(req, res, next) { - const role = req.user.role + const isAdmin = req.user.administrator const { _id, imageId } = req.body if (!_id || !imageId) { @@ -223,7 +193,7 @@ export class UserRoutes { const isSelf = (_id === req.user._id.toString()) // User can change themselves, otherwise must be super user - if (!isSelf && role !== 'executive' && role !== 'administrator') { + if (!isSelf && !isAdmin) { return next(new createError.Forbidden()) } @@ -304,9 +274,9 @@ export class UserRoutes { } deleteUser(req, res, next) { - const role = req.user.role + const isAdmin = req.user.administrator - if (role !== 'executive' && role !== 'administrator') { + if (!isAdmin) { return new createError.Forbidden() } diff --git a/server/src/bin/addUser.js b/server/src/bin/addUser.js index 27daaa1..17a03c6 100644 --- a/server/src/bin/addUser.js +++ b/server/src/bin/addUser.js @@ -15,7 +15,7 @@ new DB().connect(mongoUri).then((db) => { const User = db.User let user = new User({ - role: "administrator" + administrator: true, }) user.firstName = readlineSync.question('First name? ') user.lastName = readlineSync.question('Last name? ') diff --git a/server/src/database/schemas/user.js b/server/src/database/schemas/user.js index 2cecabc..e3c1a84 100644 --- a/server/src/database/schemas/user.js +++ b/server/src/database/schemas/user.js @@ -31,14 +31,11 @@ export let userSchema = new Schema({ }, firstName: { type: String, required: true }, lastName: { type: String, required: true }, - role: { type: String, required: true, enum: { - values: [ 'administrator', 'normal'], - message: 'enum validator failed for path `{PATH}` with value `{VALUE}`' - }}, + administrator: { type: Boolean, required: true }, }, { timestamps: true, id: false }) userSchema.methods.toClient = function(authUser) { - if (authUser === undefined) { + if (!authUser) { authUser = this } @@ -50,23 +47,7 @@ userSchema.methods.toClient = function(authUser) { thumbnailImageId: this.thumbnailImageId, firstName: this.firstName, lastName: this.lastName, - role: this.role - } - - if ((authUser.role === 'administrator' || authUser.role === 'executive') || authUser._id.equals(this._id)) { - user.zip = this.zip - user.state = this.state - user.city = this.city - user.address1 = this.address1 - user.address2 = this.address2 - user.homePhone = this.homePhone - user.cellPhone = this.cellPhone - user.ssn = this.ssn - user.dateOfBirth = this.dateOfBirth - user.dateOfHire = this.dateOfHire - user.numHouseholds = this.numHouseholds - user.t12 = this.t12 - user.aum = this.aum + administrator: this.administrator } return user diff --git a/website/package-lock.json b/website/package-lock.json index 2c388f7..0fcbd7b 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -10214,9 +10214,9 @@ } }, "react-form-binder": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/react-form-binder/-/react-form-binder-1.0.5.tgz", - "integrity": "sha512-R5oWObsNFnOCviaV5MmFDP0e+RcpPhjzVpMgvDqW3CCUdZp7XYjfyqmv1IXYlvsqavodPmxBLSFhQwxglyzJkg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/react-form-binder/-/react-form-binder-1.1.0.tgz", + "integrity": "sha512-48bPtvfHTsgi8CBbKx3FHNI2H6xwCbVHkZn+wjmTUcqfdug/M7qev6+ssvwU16USSr+IHu8bUJVD3Q0xYT8o1Q==", "requires": { "eventemitter3": "2.0.3", "prop-types": "15.6.0", diff --git a/website/package.json b/website/package.json index fe0da53..6276cac 100644 --- a/website/package.json +++ b/website/package.json @@ -10,7 +10,7 @@ "radium": "^0.22.0", "react": "^16.2.0", "react-dom": "^16.2.0", - "react-form-binder": "^1.0.5", + "react-form-binder": "^1.1.0", "react-router-dom": "^4.1.1", "regexp-pattern": "^1.0.4", "socket.io-client": "^2.0.3" diff --git a/website/src/App.js b/website/src/App.js index 02534e9..55d5a61 100644 --- a/website/src/App.js +++ b/website/src/App.js @@ -3,15 +3,20 @@ import { Login, Logout, ResetPassword, ForgotPassword, ConfirmEmail, ProtectedRo import { Home } from './Home' import { Profile } from './Profile' import { Users } from './Users' -import { HeaderButton, Column, Row, Text, Box } from 'ui' +import { HeaderButton, HeaderText, Column, Row, Text, Box } from 'ui' import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' import logoImage from 'images/logo.png' import { versionInfo } from './version' import { sizeInfo, colorInfo } from 'ui/style' import { api } from 'src/API' import { reactAutoBind } from 'auto-bind2' +import PropTypes from 'prop-types' export class App extends Component { + static propTypes = { + history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + } + constructor(props) { super(props) reactAutoBind(this) @@ -35,7 +40,16 @@ export class App extends Component { } handleLogout() { - api.logout() + // We have to use window here because App does not have history in it's props + window.location.replace('/logout') + } + + handleHome() { + window.location.replace('/') + } + + handleChangeTitle(title) { + this.setState({ title }) } render() { @@ -45,11 +59,13 @@ export class App extends Component { if (loggedInUser) { headerButtonsLeft = ( - + + + + ) headerButtonsRight = ( - ) @@ -73,7 +89,7 @@ export class App extends Component { - + ()} /> diff --git a/website/src/Auth/Login.js b/website/src/Auth/Login.js index 7fcefda..f895cd0 100644 --- a/website/src/Auth/Login.js +++ b/website/src/Auth/Login.js @@ -62,8 +62,8 @@ export class Login extends Component { api.login(obj.email, obj.password, obj.rememberMe).then((user) => { this.setState({ waitModal: false }) if (this.props.history) { - const landing = user.role === 'broker' ? '/broker-dashboard' : 'dashboard' - let url = new URLSearchParams(window.location.search).get('redirect') || landing + let url = new URLSearchParams(window.location.search).get('redirect') || '/' + try { this.props.history.replace(url) } catch (error) { @@ -73,7 +73,7 @@ export class Login extends Component { }).catch((error) => { this.setState({ waitModal: false, - messageModal: { icon: 'hold', message: `Unable to login`, detail: error.message } + messageModal: { icon: 'hand', message: `Unable to login`, detail: error.message } }) }) } diff --git a/website/src/Auth/Logout.js b/website/src/Auth/Logout.js index 4c600a1..9b19c7c 100644 --- a/website/src/Auth/Logout.js +++ b/website/src/Auth/Logout.js @@ -6,10 +6,11 @@ export class Logout extends React.Component { static propTypes = { history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), } + componentDidMount(event) { api.logout().then(() => { if (this.props.history) { - this.props.history.push('/') + this.props.history.replace('/login') } }) } diff --git a/website/src/Auth/ProtectedRoute.js b/website/src/Auth/ProtectedRoute.js index 7cbca82..0b531b3 100644 --- a/website/src/Auth/ProtectedRoute.js +++ b/website/src/Auth/ProtectedRoute.js @@ -5,7 +5,6 @@ import { api } from 'src/API' export class ProtectedRoute extends React.Component { static propTypes = { - roles: PropTypes.array, location: PropTypes.shape({ pathname: PropTypes.string, search: PropTypes.string, @@ -41,17 +40,11 @@ export class ProtectedRoute extends React.Component { // The API might be in the middle of fetching the user information // Return something and wait for login evint to fire to re-render return
- } - - let roles = this.props.roles - - if (roles && roles.includes(user.role)) { + } else if (user.administrator) { return - } else { - return } - } else { - return } + + return } } diff --git a/website/src/Modal/MessageModal.js b/website/src/Modal/MessageModal.js index f9f185d..6e54967 100644 --- a/website/src/Modal/MessageModal.js +++ b/website/src/Modal/MessageModal.js @@ -53,7 +53,7 @@ export class MessageModal extends React.Component { - + - - - + +
+ + + + + + + + + {question} + + + + + + +
) diff --git a/website/src/ui/Box.js b/website/src/ui/Box.js index bf1456d..fc8f54e 100644 --- a/website/src/ui/Box.js +++ b/website/src/ui/Box.js @@ -6,25 +6,27 @@ import React, { Component } from 'react' class Box extends Component { static propTypes = { - borderTop: PropTypes.string, - borderBottom: PropTypes.string, - borderRight: PropTypes.string, - borderLeft: PropTypes.string, - border: PropTypes.string, + borderTop: PropTypes.object, + borderBottom: PropTypes.object, + borderRight: PropTypes.object, + borderLeft: PropTypes.object, + border: PropTypes.object, radius: PropTypes.number, - color: PropTypes.string, + background: PropTypes.string, children: PropTypes.node, } render() { - const { children, color, borderTop, borderBottom, borderLeft, borderRight, border, radius } = this.props + const { children, background, borderTop, borderBottom, borderLeft, + borderRight, border, radius } = this.props return (
+ border ? { border } : { borderTop, borderBottom, borderLeft, borderRight }, + ]}> {children}
) diff --git a/website/src/ui/Button.js b/website/src/ui/Button.js index cab6c5a..c97ed19 100644 --- a/website/src/ui/Button.js +++ b/website/src/ui/Button.js @@ -1,8 +1,7 @@ import Radium from 'radium' import PropTypes from 'prop-types' import React, { Component } from 'react' -import { colorInfo, sizeInfo } from './style' -import { Text } from '.' +import { fontInfo, colorInfo, sizeInfo } from './style' class Button extends Component { static propTypes = { @@ -22,7 +21,11 @@ class Button extends Component { static style = { button: { + fontSize: fontInfo.size.medium, + fontFamily: fontInfo.family, + color: fontInfo.color.inverse, height: sizeInfo.buttonHeight, + borderWidth: 0, borderRadius: 10, background: colorInfo.buttonBackgroundHover, verticalAlign: 'middle', @@ -35,7 +38,6 @@ class Button extends Component { background: colorInfo.disabledButtonBackground, }, ':active': { - borderWidth: 0, background: colorInfo.buttonBackgroundActive, } } @@ -45,12 +47,10 @@ class Button extends Component { const { name, submit, visible, disabled, width, text, onClick } = this.props return ( - + onClick={onClick} value={text} /> ) } } diff --git a/website/src/ui/HeaderText.js b/website/src/ui/HeaderText.js new file mode 100644 index 0000000..0d59c17 --- /dev/null +++ b/website/src/ui/HeaderText.js @@ -0,0 +1,35 @@ +import Radium from 'radium' +import PropTypes from 'prop-types' +import React, { Component } from 'react' +import { sizeInfo, fontInfo } from 'ui/style' + +class HeaderText extends Component { + static propTypes = { + text: PropTypes.string, + } + + static style = { + position: 'relative', + top: 3, + display: 'inline-block', + fontSize: fontInfo.size.huge, + fontFamily: fontInfo.family, + color: fontInfo.color.normal, + textAlign: 'left', + background: 'transparent', + verticalAlign: 'middle', + borderWidth: 0, + paddingLeft: 10, + } + + render() { + const height = sizeInfo.headerHeight - sizeInfo.headerBorderWidth + const { text } = this.props + + return ( +
{text}
+ ) + } +} + +export default Radium(HeaderText) diff --git a/website/src/ui/Icon.js b/website/src/ui/Icon.js index 26bc6ee..ec5a00f 100644 --- a/website/src/ui/Icon.js +++ b/website/src/ui/Icon.js @@ -19,10 +19,15 @@ export default class Icon extends Component { static svgs = { logout: require('icons/logout.svg'), profile: require('icons/profile.svg'), - hold: require('icons/hold.svg'), + admin: require('icons/admin.svg'), + hand: require('icons/hand.svg'), users: require('icons/users.svg'), teams: require('icons/teams.svg'), system: require('icons/system.svg'), + confirmed: require('icons/confirmed.svg'), + help: require('icons/help.svg'), + warning: require('icons/warning.svg'), + edit: require('icons/edit.svg'), placeholder: require('icons/placeholder.svg'), } diff --git a/website/src/ui/Input.js b/website/src/ui/Input.js index 40a92dc..372f93d 100644 --- a/website/src/ui/Input.js +++ b/website/src/ui/Input.js @@ -2,6 +2,8 @@ import Radium from 'radium' import PropTypes from 'prop-types' import React, { Component } from 'react' +// See https://stackoverflow.com/a/6891562/576235 for why we wrap the element + class Input extends Component { static propTypes = { password: PropTypes.bool, @@ -35,7 +37,11 @@ class Input extends Component { margin: 0, width: '100%', outline: 'none', - } + ':disabled': { + color: '#AAAAAA', + background: '#ffffff' + } + }, } render() { diff --git a/website/src/ui/List.js b/website/src/ui/List.js index 1964a5b..c45f189 100644 --- a/website/src/ui/List.js +++ b/website/src/ui/List.js @@ -11,7 +11,6 @@ class List extends Component { render() { const { children } = this.props - const listGapItem = (
) return (
- - {listGapItem} -
+ +
{children}
- {listGapItem}
) @@ -93,7 +96,8 @@ List.Text = Radium(class ListText extends Component { color: fontInfo.color.normal, align: 'left', verticalAlign: 'middle', - textAlign: 'left' + textAlign: 'left', + cursor: 'default', }}>{children} ) } diff --git a/website/src/ui/index.js b/website/src/ui/index.js index 06e0c77..411e348 100644 --- a/website/src/ui/index.js +++ b/website/src/ui/index.js @@ -2,6 +2,7 @@ export { default as Anime } from './Anime' export { default as Box } from './Box' export { default as Button } from './Button' export { default as HeaderButton } from './HeaderButton' +export { default as HeaderText } from './HeaderText' export { default as PanelButton } from './PanelButton' export { default as Checkbox } from './Checkbox' export { default as Input } from './Input'