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 } );