From ffbf3bc826f78aa7541df67c84c9eb65cd3fe529 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 22 Jan 2018 17:08:33 -0800 Subject: [PATCH 1/8] fix(updateMyCurrentChallenge): Bad mongo id will return user error Mark these errors to be reported to the user instead of logged as a server fault --- server/boot/authentication.js | 25 +++++++--------------- server/boot/settings.js | 31 +++++++++++++++++----------- server/utils/create-handled-error.js | 1 + server/utils/middleware.js | 16 ++++++++++++++ 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/server/boot/authentication.js b/server/boot/authentication.js index eac0e68b8b..c40c864345 100644 --- a/server/boot/authentication.js +++ b/server/boot/authentication.js @@ -3,13 +3,13 @@ import { Observable } from 'rx'; import dedent from 'dedent'; // import debugFactory from 'debug'; import { isEmail } from 'validator'; -import { check, validationResult } from 'express-validator/check'; +import { check } from 'express-validator/check'; -import { ifUserRedirectTo } from '../utils/middleware'; import { - wrapHandledError, - createValidatorErrorFormatter -} from '../utils/create-handled-error.js'; + ifUserRedirectTo, + createValidatorErrorHandler +} from '../utils/middleware'; +import { wrapHandledError } from '../utils/create-handled-error.js'; const isSignUpDisabled = !!process.env.DISABLE_SIGNUP; // const debug = debugFactory('fcc:boot:auth'); @@ -82,13 +82,6 @@ module.exports = function enableAuthentication(app) { token: authTokenId } = {} } = req; - const validation = validationResult(req) - .formatWith(createValidatorErrorFormatter('errors', '/email-signup')); - - if (!validation.isEmpty()) { - const errors = validation.array(); - return next(errors.pop()); - } const email = User.decodeEmail(encodedEmail); if (!isEmail(email)) { @@ -188,6 +181,7 @@ module.exports = function enableAuthentication(app) { '/passwordless-auth', ifUserRedirect, passwordlessGetValidators, + createValidatorErrorHandler('errors', '/email-signup'), getPasswordlessAuth ); @@ -198,12 +192,6 @@ module.exports = function enableAuthentication(app) { ]; function postPasswordlessAuth(req, res, next) { const { body: { email } = {} } = req; - const validation = validationResult(req) - .formatWith(createValidatorErrorFormatter('errors', '/email-signup')); - if (!validation.isEmpty()) { - const errors = validation.array(); - return next(errors.pop()); - } return User.findOne$({ where: { email } }) .flatMap(_user => Observable.if( @@ -222,6 +210,7 @@ module.exports = function enableAuthentication(app) { '/passwordless-auth', ifUserRedirect, passwordlessPostValidators, + createValidatorErrorHandler('errors', '/email-signup'), postPasswordlessAuth ); diff --git a/server/boot/settings.js b/server/boot/settings.js index 9264d16fcb..1df8d1b2e8 100644 --- a/server/boot/settings.js +++ b/server/boot/settings.js @@ -1,6 +1,9 @@ -import { isMongoId } from 'validator'; +import { check } from 'express-validator/check'; -import { ifNoUser401 } from '../utils/middleware'; +import { + ifNoUser401, + createValidatorErrorHandler +} from '../utils/middleware'; import supportedLanguages from '../../common/utils/supported-languages.js'; export default function settingsController(app) { @@ -51,11 +54,14 @@ export default function settingsController(app) { ); } + const updateMyCurrentChallengeValidators = [ + check('currentChallengeId') + .isMongoId() + .withMessage('currentChallengeId is not a valid challenge ID') + ]; + function updateMyCurrentChallenge(req, res, next) { const { user, body: { currentChallengeId } } = req; - if (!isMongoId('' + currentChallengeId)) { - return next(new Error(`${currentChallengeId} is not a valid ObjectId`)); - } return user.update$({ currentChallengeId }).subscribe( () => res.json({ message: @@ -65,6 +71,14 @@ export default function settingsController(app) { ); } + api.post( + '/update-my-current-challenge', + ifNoUser401, + updateMyCurrentChallengeValidators, + createValidatorErrorHandler('errors'), + updateMyCurrentChallenge + ); + function updateMyTheme(req, res, next) { req.checkBody('theme', 'Theme is invalid.').isLength({ min: 4 }); const { body: { theme } } = req; @@ -117,13 +131,6 @@ export default function settingsController(app) { ifNoUser401, updateMyLang ); - - api.post( - '/update-my-current-challenge', - ifNoUser401, - updateMyCurrentChallenge - ); - api.post( '/update-my-theme', ifNoUser401, diff --git a/server/utils/create-handled-error.js b/server/utils/create-handled-error.js index d5e9a00934..30eacd11ea 100644 --- a/server/utils/create-handled-error.js +++ b/server/utils/create-handled-error.js @@ -18,6 +18,7 @@ export function wrapHandledError(err, { return err; } +// for use with express-validator error formatter export const createValidatorErrorFormatter = (type, redirectTo, status) => ({ msg }) => wrapHandledError( new Error(msg), diff --git a/server/utils/middleware.js b/server/utils/middleware.js index 1aa455a0fb..f38afbdb37 100644 --- a/server/utils/middleware.js +++ b/server/utils/middleware.js @@ -1,4 +1,7 @@ import dedent from 'dedent'; +import { validationResult } from 'express-validator/check'; + +import { createValidatorErrorFormatter } from './create-handled-error.js'; export function ifNoUserRedirectTo(url, message, type = 'errors') { return function(req, res, next) { @@ -56,3 +59,16 @@ export function ifUserRedirectTo(path = '/', status) { return next(); }; } + +// for use with express-validator error formatter +export const createValidatorErrorHandler = (...args) => (req, res, next) => { + const validation = validationResult(req) + .formatWith(createValidatorErrorFormatter(...args)); + + if (!validation.isEmpty()) { + const errors = validation.array(); + return next(errors.pop()); + } + + return next(); +}; From 4d545a018cebfaee6d02f382ffd5a696904d6042 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 26 Jan 2018 14:37:40 -0800 Subject: [PATCH 2/8] fix(nightModeEpic): Colocate in app This moves epic into main app and prevents it running server side --- client/epics/night-mode-epic.js | 59 -------------------------- common/app/redux/night-mode-epic.js | 64 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 59 deletions(-) delete mode 100644 client/epics/night-mode-epic.js create mode 100644 common/app/redux/night-mode-epic.js diff --git a/client/epics/night-mode-epic.js b/client/epics/night-mode-epic.js deleted file mode 100644 index 58b4b01354..0000000000 --- a/client/epics/night-mode-epic.js +++ /dev/null @@ -1,59 +0,0 @@ -import _ from 'lodash'; -import { Observable } from 'rx'; -import { ofType } from 'redux-epic'; -import store from 'store'; - -import { themes } from '../../common/utils/themes.js'; -import { postJSON$ } from '../../common/utils/ajax-stream.js'; -import { - types, - - postThemeComplete, - createErrorObservable, - - themeSelector, - usernameSelector, - csrfSelector -} from '../../common/app/redux'; - -function persistTheme(theme) { - store.set('fcc-theme', theme); -} - -export default function nightModeSaga( - actions, - { getState }, - { document: { body } } -) { - const toggleBodyClass = actions - ::ofType( - types.fetchUser.complete, - types.toggleNightMode, - types.postThemeComplete - ) - .map(_.flow(getState, themeSelector)) - // catch existing night mode users - .do(persistTheme) - .do(theme => { - if (theme === themes.night) { - body.classList.add(themes.night); - } else { - body.classList.remove(themes.night); - } - }) - .ignoreElements(); - - const postThemeEpic = actions::ofType(types.toggleNightMode) - .debounce(250) - .flatMapLatest(() => { - const _csrf = csrfSelector(getState()); - const theme = themeSelector(getState()); - const username = usernameSelector(getState()); - return postJSON$('/update-my-theme', { _csrf, theme }) - .pluck('updatedTo') - .map(theme => postThemeComplete(username, theme)) - .catch(createErrorObservable); - }); - - return Observable.merge(toggleBodyClass, postThemeEpic); -} diff --git a/common/app/redux/night-mode-epic.js b/common/app/redux/night-mode-epic.js new file mode 100644 index 0000000000..b077bfaf5e --- /dev/null +++ b/common/app/redux/night-mode-epic.js @@ -0,0 +1,64 @@ +import _ from 'lodash'; +import { Observable } from 'rx'; +import { ofType } from 'redux-epic'; +import store from 'store'; + +import { themes } from '../../utils/themes.js'; +import { postJSON$ } from '../../utils/ajax-stream.js'; +import { + types, + + postThemeComplete, + createErrorObservable, + + themeSelector, + usernameSelector, + csrfSelector +} from './index.js'; + +function persistTheme(theme) { + store.set('fcc-theme', theme); +} + +export default function nightModeEpic( + actions, + { getState }, + { document } +) { + return Observable.of(document) + // if document is undefined we do nothing (ssr trap) + .filter(Boolean) + .flatMap(({ body }) => { + const toggleBodyClass = actions + ::ofType( + types.fetchUser.complete, + types.toggleNightMode, + types.postThemeComplete + ) + .map(_.flow(getState, themeSelector)) + // catch existing night mode users + .do(persistTheme) + .do(theme => { + if (theme === themes.night) { + body.classList.add(themes.night); + } else { + body.classList.remove(themes.night); + } + }) + .ignoreElements(); + + const postThemeEpic = actions::ofType(types.toggleNightMode) + .debounce(250) + .flatMapLatest(() => { + const _csrf = csrfSelector(getState()); + const theme = themeSelector(getState()); + const username = usernameSelector(getState()); + return postJSON$('/update-my-theme', { _csrf, theme }) + .pluck('updatedTo') + .map(theme => postThemeComplete(username, theme)) + .catch(createErrorObservable); + }); + + return Observable.merge(toggleBodyClass, postThemeEpic); + }); +} From 040d49d61292984ac709abd51379994f4ab9cb73 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 26 Jan 2018 19:15:23 -0800 Subject: [PATCH 3/8] feat(Flash): Should reflect express style --- common/app/Flash/Flash.jsx | 8 +++--- common/app/Flash/redux/index.js | 9 +++---- common/app/Flash/redux/utils.js | 43 +++++++++++++++++++++++++-------- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/common/app/Flash/Flash.jsx b/common/app/Flash/Flash.jsx index 52a549b0c7..6582e890ed 100644 --- a/common/app/Flash/Flash.jsx +++ b/common/app/Flash/Flash.jsx @@ -11,19 +11,19 @@ import { } from './redux'; const propTypes = { - alertType: PropTypes.oneOf(Object.keys(alertTypes)), clickOnClose: PropTypes.func.isRequired, - message: PropTypes.string + message: PropTypes.string, + type: PropTypes.oneOf(Object.keys(alertTypes)) }; const mapStateToProps = latestMessageSelector; const mapDispatchToProps = { clickOnClose }; -export function Flash({ alertType, clickOnClose, message }) { +export function Flash({ type, clickOnClose, message }) { if (!message) { return null; } return ( -
+

{ message } diff --git a/common/app/Flash/redux/index.js b/common/app/Flash/redux/index.js index 3b0595654c..cd217b8c1e 100644 --- a/common/app/Flash/redux/index.js +++ b/common/app/Flash/redux/index.js @@ -11,6 +11,8 @@ import * as utils from './utils.js'; import getMessagesEpic from './get-messages-epic.js'; import ns from '../ns.json'; +// export all the utils +export { utils }; export const epics = [getMessagesEpic]; export const types = createTypes([ 'clickOnClose', @@ -45,13 +47,10 @@ export default composeReducers( ), function metaReducer(state = defaultState, action) { if (utils.isFlashAction(action)) { - const { payload: { alertType, message } } = utils.getFlashAction(action); + const { payload } = utils.getFlashAction(action); return [ ...state, - { - alertType: utils.normalizeAlertType(alertType), - message: _.escape(message) - } + ...payload ]; } return state; diff --git a/common/app/Flash/redux/utils.js b/common/app/Flash/redux/utils.js index e3b22dec9f..408ad1bdb2 100644 --- a/common/app/Flash/redux/utils.js +++ b/common/app/Flash/redux/utils.js @@ -9,21 +9,44 @@ export const alertTypes = _.keyBy(_.identity)([ export const normalizeAlertType = alertType => alertTypes[alertType] || 'info'; -export const getFlashAction = _.flow( - _.property('meta'), - _.property('flash') -); - -export const isFlashAction = _.flow( - getFlashAction, - Boolean -); +// interface ExpressFlash { +// [alertType]: [String...] +// } +// interface StackFlash { +// type: AlertType, +// message: String +// } export const expressToStack = _.flow( _.toPairs, _.flatMap(([ type, messages ]) => messages.map(msg => ({ message: msg, - alertType: normalizeAlertType(type) + type: normalizeAlertType(type) }))) ); +export const isExpressFlash = _.flow( + _.keys, + _.every(type => alertTypes[type]) +); + +export const getFlashAction = _.flow( + _.property('meta'), + _.property('flash') +); + +// FlashMessage +// createFlashMetaAction(payload: ExpressFlash|StackFlash +export const createFlashMetaAction = payload => { + if (isExpressFlash(payload)) { + payload = expressToStack(payload); + } else { + payload = [payload]; + } + return { flash: { payload } }; +}; + +export const isFlashAction = _.flow( + getFlashAction, + Boolean +); From 8025f96fa177f0d343297ee6e6e5b54cd9011380 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 29 Jan 2018 11:22:39 -0800 Subject: [PATCH 4/8] fix(AjaxStream): Parse json response on error --- common/utils/ajax-stream.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/common/utils/ajax-stream.js b/common/utils/ajax-stream.js index b5588c29a9..dee70b3b6f 100644 --- a/common/utils/ajax-stream.js +++ b/common/utils/ajax-stream.js @@ -64,11 +64,27 @@ function getCORSRequest() { } } +function parseXhrResponse(responseType, xhr) { + switch (responseType) { + case 'json': + if ('response' in xhr) { + return xhr.responseType ? + xhr.response : + JSON.parse(xhr.response || xhr.responseText || 'null'); + } else { + return JSON.parse(xhr.responseText || 'null'); + } + case 'xml': + return xhr.responseXML; + case 'text': + default: + return ('response' in xhr) ? xhr.response : xhr.responseText; + } +} + function normalizeAjaxSuccessEvent(e, xhr, settings) { - var response = ('response' in xhr) ? xhr.response : xhr.responseText; - response = settings.responseType === 'json' ? JSON.parse(response) : response; return { - response: response, + response: parseXhrResponse(settings.responseType || xhr.responseType, xhr), status: xhr.status, responseType: xhr.responseType, xhr: xhr, @@ -266,7 +282,8 @@ export function postJSON$(url, body) { headers: { 'Content-Type': 'application/json', Accept: 'application/json' - } + }, + normalizeError: (e, xhr) => parseXhrResponse('json', xhr) }) .map(({ response }) => response); } @@ -291,6 +308,7 @@ export function getJSON$(url) { headers: { 'Content-Type': 'application/json', Accept: 'application/json' - } + }, + normalizeError: (e, xhr) => parseXhrResponse('json', xhr) }).map(({ response }) => response); } From b8f8ea80cf10f9df66a37fb5cd6f7f25bd2a1137 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 29 Jan 2018 11:24:13 -0800 Subject: [PATCH 5/8] feat(express): Add sendFlash to send a flash message json --- server/middlewares/express-extensions.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/middlewares/express-extensions.js b/server/middlewares/express-extensions.js index d875260fa1..69b4cce108 100644 --- a/server/middlewares/express-extensions.js +++ b/server/middlewares/express-extensions.js @@ -9,6 +9,12 @@ export default function() { res.renderWithoutFlash = res.render; // render to observable stream using build in render res.render$ = Observable.fromNodeCallback(res.render, res); + res.sendFlash = (type, message) => { + if (type && message) { + req.flash(type, message); + } + return res.json(req.flash()); + }; next(); }; } From ae3ccdd672d03410de390e1cd84f24b885ba981d Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 29 Jan 2018 11:26:24 -0800 Subject: [PATCH 6/8] fix(user/settings): Add theme server validations --- client/epics/index.js | 2 -- common/app/redux/index.js | 21 ++++++++++++++++---- common/app/redux/night-mode-epic.js | 20 +++++++++---------- common/models/user.js | 4 +--- server/boot/settings.js | 29 +++++++++++++++------------- server/utils/create-handled-error.js | 5 +++-- 6 files changed, 47 insertions(+), 34 deletions(-) diff --git a/client/epics/index.js b/client/epics/index.js index d17b5a70e5..c21ccd687b 100644 --- a/client/epics/index.js +++ b/client/epics/index.js @@ -2,7 +2,6 @@ import analyticsEpic from './analytics-epic.js'; import errEpic from './err-epic.js'; import hardGoToEpic from './hard-go-to-epic.js'; import mouseTrapEpic from './mouse-trap-epic.js'; -import nightModeEpic from './night-mode-epic.js'; import titleEpic from './title-epic.js'; export default [ @@ -10,6 +9,5 @@ export default [ errEpic, hardGoToEpic, mouseTrapEpic, - nightModeEpic, titleEpic ]; diff --git a/common/app/redux/index.js b/common/app/redux/index.js index b74c6abd85..6b53c6b413 100644 --- a/common/app/redux/index.js +++ b/common/app/redux/index.js @@ -12,9 +12,11 @@ import { createSelector } from 'reselect'; import fetchUserEpic from './fetch-user-epic.js'; import updateMyCurrentChallengeEpic from './update-my-challenge-epic.js'; import fetchChallengesEpic from './fetch-challenges-epic.js'; +import nightModeEpic from './night-mode-epic.js'; import { createFilesMetaCreator } from '../files'; import { updateThemeMetacreator, entitiesSelector } from '../entities'; +import { utils } from '../Flash/redux'; import { types as challenges } from '../routes/Challenges/redux'; import { challengeToFiles } from '../routes/Challenges/utils'; @@ -23,8 +25,9 @@ import ns from '../ns.json'; import { themes, invertTheme } from '../../utils/themes.js'; export const epics = [ - fetchUserEpic, fetchChallengesEpic, + fetchUserEpic, + nightModeEpic, updateMyCurrentChallengeEpic ]; @@ -48,7 +51,7 @@ export const types = createTypes([ // night mode 'toggleNightMode', - 'postThemeComplete' + createAsyncTypes('postTheme') ], ns); const throwIfUndefined = () => { @@ -130,6 +133,7 @@ export const createErrorObservable = error => Observable.just({ type: types.handleError, error }); +// use sparingly // doActionOnError( // actionCreator: (() => Action|Null) // ) => (error: Error) => Observable[Action] @@ -147,9 +151,18 @@ export const toggleNightMode = createAction( (username, theme) => updateThemeMetacreator(username, invertTheme(theme)) ); export const postThemeComplete = createAction( - types.postThemeComplete, + types.postTheme.complete, null, - updateThemeMetacreator + utils.createFlashMetaAction +); + +export const postThemeError = createAction( + types.postTheme.error, + null, + (username, theme, err) => ({ + ...updateThemeMetacreator(username, invertTheme(theme)), + ...utils.createFlashMetaAction(err) + }) ); const defaultState = { diff --git a/common/app/redux/night-mode-epic.js b/common/app/redux/night-mode-epic.js index b077bfaf5e..2449d1735a 100644 --- a/common/app/redux/night-mode-epic.js +++ b/common/app/redux/night-mode-epic.js @@ -6,14 +6,12 @@ import store from 'store'; import { themes } from '../../utils/themes.js'; import { postJSON$ } from '../../utils/ajax-stream.js'; import { - types, - + csrfSelector, postThemeComplete, - createErrorObservable, - + postThemeError, themeSelector, - usernameSelector, - csrfSelector + types, + usernameSelector } from './index.js'; function persistTheme(theme) { @@ -33,7 +31,8 @@ export default function nightModeEpic( ::ofType( types.fetchUser.complete, types.toggleNightMode, - types.postThemeComplete + types.postTheme.complete, + types.postTheme.error ) .map(_.flow(getState, themeSelector)) // catch existing night mode users @@ -54,9 +53,10 @@ export default function nightModeEpic( const theme = themeSelector(getState()); const username = usernameSelector(getState()); return postJSON$('/update-my-theme', { _csrf, theme }) - .pluck('updatedTo') - .map(theme => postThemeComplete(username, theme)) - .catch(createErrorObservable); + .map(postThemeComplete) + .catch(err => { + return Observable.of(postThemeError(username, theme, err)); + }); }); return Observable.merge(toggleBodyClass, postThemeEpic); diff --git a/common/models/user.js b/common/models/user.js index 2dc6408f20..12b057eb72 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -703,9 +703,7 @@ module.exports = function(User) { ); return Promise.reject(err); } - return this.update$({ theme }) - .map({ updatedTo: theme }) - .toPromise(); + return this.update$({ theme }).toPromise(); }; // deprecated. remove once live diff --git a/server/boot/settings.js b/server/boot/settings.js index 1df8d1b2e8..78c8520176 100644 --- a/server/boot/settings.js +++ b/server/boot/settings.js @@ -5,6 +5,7 @@ import { createValidatorErrorHandler } from '../utils/middleware'; import supportedLanguages from '../../common/utils/supported-languages.js'; +import { themes } from '../../common/utils/themes.js'; export default function settingsController(app) { const api = app.loopback.Router(); @@ -79,22 +80,29 @@ export default function settingsController(app) { updateMyCurrentChallenge ); + const updateMyThemeValidators = [ + check('theme') + .isIn(Object.keys(themes)) + .withMessage('Theme is invalid.') + ]; function updateMyTheme(req, res, next) { - req.checkBody('theme', 'Theme is invalid.').isLength({ min: 4 }); const { body: { theme } } = req; - const errors = req.validationErrors(true); - if (errors) { - return res.status(403).json({ errors }); - } if (req.user.theme === theme) { - return res.json({ msg: 'Theme already set' }); + return res.sendFlash('info', 'Theme already set'); } - return req.user.updateTheme('' + theme) + return req.user.updateTheme(theme) .then( - data => res.json(data), + () => res.sendFlash('info', 'Your theme has been updated'), next ); } + api.post( + '/update-my-theme', + ifNoUser401, + updateMyThemeValidators, + createValidatorErrorHandler('errors'), + updateMyTheme + ); api.post( '/toggle-available-for-hire', @@ -131,11 +139,6 @@ export default function settingsController(app) { ifNoUser401, updateMyLang ); - api.post( - '/update-my-theme', - ifNoUser401, - updateMyTheme - ); app.use(api); } diff --git a/server/utils/create-handled-error.js b/server/utils/create-handled-error.js index 30eacd11ea..1d282319d1 100644 --- a/server/utils/create-handled-error.js +++ b/server/utils/create-handled-error.js @@ -19,13 +19,14 @@ export function wrapHandledError(err, { } // for use with express-validator error formatter -export const createValidatorErrorFormatter = (type, redirectTo, status) => +export const createValidatorErrorFormatter = (type, redirectTo) => ({ msg }) => wrapHandledError( new Error(msg), { type, message: msg, redirectTo, - status + // we default to 400 as these are malformed requests + status: 400 } ); From 1ee9d9259c120ffd881acce10b2781dfec5604dd Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 29 Jan 2018 11:34:44 -0800 Subject: [PATCH 7/8] feat(Flash): Normalize flash types with object help prevent typo errors --- common/app/Flash/Flash.jsx | 2 +- common/app/Flash/redux/utils.js | 11 +---------- common/utils/flash.js | 10 ++++++++++ server/boot/settings.js | 9 +++++---- 4 files changed, 17 insertions(+), 15 deletions(-) create mode 100644 common/utils/flash.js diff --git a/common/app/Flash/Flash.jsx b/common/app/Flash/Flash.jsx index 6582e890ed..0e9f4b5ae0 100644 --- a/common/app/Flash/Flash.jsx +++ b/common/app/Flash/Flash.jsx @@ -4,7 +4,7 @@ import { CloseButton } from 'react-bootstrap'; import { connect } from 'react-redux'; import ns from './ns.json'; -import { alertTypes } from './redux/utils.js'; +import { alertTypes } from '../../utils/flash.js'; import { latestMessageSelector, clickOnClose diff --git a/common/app/Flash/redux/utils.js b/common/app/Flash/redux/utils.js index 408ad1bdb2..2ebbfb9332 100644 --- a/common/app/Flash/redux/utils.js +++ b/common/app/Flash/redux/utils.js @@ -1,13 +1,5 @@ import _ from 'lodash/fp'; - -export const alertTypes = _.keyBy(_.identity)([ - 'success', - 'info', - 'warning', - 'danger' -]); - -export const normalizeAlertType = alertType => alertTypes[alertType] || 'info'; +import { alertTypes, normalizeAlertType } from '../../../utils/flash.js'; // interface ExpressFlash { // [alertType]: [String...] @@ -16,7 +8,6 @@ export const normalizeAlertType = alertType => alertTypes[alertType] || 'info'; // type: AlertType, // message: String // } - export const expressToStack = _.flow( _.toPairs, _.flatMap(([ type, messages ]) => messages.map(msg => ({ diff --git a/common/utils/flash.js b/common/utils/flash.js new file mode 100644 index 0000000000..b965b3680e --- /dev/null +++ b/common/utils/flash.js @@ -0,0 +1,10 @@ +import _ from 'lodash'; + +export const alertTypes = _.keyBy([ + 'success', + 'info', + 'warning', + 'danger' +], _.identity); + +export const normalizeAlertType = alertType => alertTypes[alertType] || 'info'; diff --git a/server/boot/settings.js b/server/boot/settings.js index 78c8520176..453a0d3f8c 100644 --- a/server/boot/settings.js +++ b/server/boot/settings.js @@ -6,6 +6,7 @@ import { } from '../utils/middleware'; import supportedLanguages from '../../common/utils/supported-languages.js'; import { themes } from '../../common/utils/themes.js'; +import { alertTypes } from '../../common/utils/flash.js'; export default function settingsController(app) { const api = app.loopback.Router(); @@ -76,7 +77,7 @@ export default function settingsController(app) { '/update-my-current-challenge', ifNoUser401, updateMyCurrentChallengeValidators, - createValidatorErrorHandler('errors'), + createValidatorErrorHandler(alertTypes.danger), updateMyCurrentChallenge ); @@ -88,11 +89,11 @@ export default function settingsController(app) { function updateMyTheme(req, res, next) { const { body: { theme } } = req; if (req.user.theme === theme) { - return res.sendFlash('info', 'Theme already set'); + return res.sendFlash(alertTypes.info, 'Theme already set'); } return req.user.updateTheme(theme) .then( - () => res.sendFlash('info', 'Your theme has been updated'), + () => res.sendFlash(alertTypes.info, 'Your theme has been updated'), next ); } @@ -100,7 +101,7 @@ export default function settingsController(app) { '/update-my-theme', ifNoUser401, updateMyThemeValidators, - createValidatorErrorHandler('errors'), + createValidatorErrorHandler(alertTypes.danger), updateMyTheme ); From 660f78896d6149ab8236b01822e1ec1eb9cfa780 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 29 Jan 2018 18:53:33 -0800 Subject: [PATCH 8/8] fix(settings/updateEmail): Show message from server --- common/app/routes/Settings/redux/index.js | 17 +++++++++++++++-- server/boot/settings.js | 21 +++++++++++++++------ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/common/app/routes/Settings/redux/index.js b/common/app/routes/Settings/redux/index.js index fec849ee60..c244aacce0 100644 --- a/common/app/routes/Settings/redux/index.js +++ b/common/app/routes/Settings/redux/index.js @@ -2,11 +2,13 @@ import { isLocationAction } from 'redux-first-router'; import { addNS, createAction, + createAsyncTypes, createTypes } from 'berkeleys-redux-utils'; import userUpdateEpic from './update-user-epic.js'; import ns from '../ns.json'; +import { utils } from '../../../Flash/redux'; export const epics = [ userUpdateEpic @@ -14,7 +16,7 @@ export const epics = [ export const types = createTypes([ 'toggleUserFlag', - 'updateMyEmail', + createAsyncTypes('updateMyEmail'), 'updateMyLang', 'onRouteSettings', 'onRouteUpdateEmail' @@ -24,7 +26,18 @@ export const types = createTypes([ export const onRouteSettings = createAction(types.onRouteSettings); export const onRouteUpdateEmail = createAction(types.onRouteUpdateEmail); export const toggleUserFlag = createAction(types.toggleUserFlag); -export const updateMyEmail = createAction(types.updateMyEmail); +export const updateMyEmail = createAction(types.updateMyEmail.start); +export const updateMyEmailComplete = createAction( + types.updateMyEmail.complete, + null, + utils.createFlashMetaAction +); + +export const updateMyEmailError = createAction( + types.updateMyEmail.error, + null, + utils.createFlashMetaAction +); export const updateMyLang = createAction( types.updateMyLang, diff --git a/server/boot/settings.js b/server/boot/settings.js index 453a0d3f8c..fb4a12467a 100644 --- a/server/boot/settings.js +++ b/server/boot/settings.js @@ -24,15 +24,29 @@ export default function settingsController(app) { ); }; + const updateMyEmailValidators = [ + check('email') + .isEmail() + .withMessage('Email format is invalid.') + ]; + function updateMyEmail(req, res, next) { const { user, body: { email } } = req; return user.requestUpdateEmail(email) .subscribe( - (message) => res.json({ message }), + message => res.sendFlash(alertTypes.info, message), next ); } + api.post( + '/update-my-email', + ifNoUser401, + updateMyEmailValidators, + createValidatorErrorHandler(alertTypes.danger), + updateMyEmail + ); + function updateMyLang(req, res, next) { const { user, body: { lang } = {} } = req; const langName = supportedLanguages[lang]; @@ -130,11 +144,6 @@ export default function settingsController(app) { ifNoUser401, toggleUserFlag('sendQuincyEmail') ); - api.post( - '/update-my-email', - ifNoUser401, - updateMyEmail - ); api.post( '/update-my-lang', ifNoUser401,