diff --git a/app.js b/app.js index 3e257cb3..a6060d9b 100644 --- a/app.js +++ b/app.js @@ -239,7 +239,6 @@ app.use('/account', accountLegacyIntegration); app.all('/rest/*', (req, res, next) => { - console.log('njr'); req.needsJSONResponse = true; next(); }); @@ -264,9 +263,9 @@ app.use((req, res, next) => { // error handlers -// development error handler -// will print stacktrace if (app.get('env') === 'development') { + // development error handler + // will print stacktrace app.use((err, req, res, next) => { if (!err) { return next(); @@ -278,12 +277,13 @@ if (app.get('env') === 'development') { error: err }; - if (err instanceof InteroperableError) { + if (err instanceof interoperableErrors.InteroperableError) { resp.type = err.type; resp.data = err.data; } res.status(err.status || 500).json(resp); + } else { res.status(err.status || 500); res.render('error', { @@ -293,34 +293,36 @@ if (app.get('env') === 'development') { } }); -} - -// production error handler -// no stacktraces leaked to user -app.use((err, req, res, next) => { - if (!err) { - return next(); - } - - if (req.needsJSONResponse) { - const resp = { - message: err.message, - error: {} - }; - - if (err instanceof interoperableErrors.InteroperableError) { - resp.type = err.type; - resp.data = err.data; +} else { + // production error handler + // no stacktraces leaked to user + app.use((err, req, res, next) => { + if (!err) { + return next(); } - res.status(err.status || 500).json(resp); - } else { - res.status(err.status || 500); - res.render('error', { - message: err.message, - error: {} - }); - } -}); + if (req.needsJSONResponse) { + const resp = { + message: err.message, + error: {} + }; + + if (err instanceof interoperableErrors.InteroperableError) { + resp.type = err.type; + resp.data = err.data; + } + + res.status(err.status || 500).json(resp); + + } else { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: {} + }); + } + }); +} + module.exports = app; diff --git a/client/package.json b/client/package.json index 977455d2..462a0594 100644 --- a/client/package.json +++ b/client/package.json @@ -16,34 +16,34 @@ "license": "GPL-3.0", "homepage": "https://mailtrain.org/", "dependencies": { - "axios": "^0.16.1", + "axios": "^0.16.2", "datatables.net": "^1.10.15", "datatables.net-bs": "^1.10.15", "datatables.net-select": "^1.2.2", "datatables.net-select-bs": "^1.2.2", - "i18next": "^8.3.0", - "i18next-xhr-backend": "^1.4.1", + "i18next": "^8.4.3", + "i18next-xhr-backend": "^1.4.2", "immutable": "^3.8.1", "owasp-password-strength-test": "github:bures/owasp-password-strength-test", "prop-types": "^15.5.10", - "react": "^15.5.4", - "react-dom": "^15.5.4", - "react-i18next": "^4.1.0", + "react": "^15.6.1", + "react-dom": "^15.6.1", + "react-i18next": "^4.6.1", "react-router-dom": "^4.1.1", "url-parse": "^1.1.9" }, "devDependencies": { "babel-cli": "^6.24.1", - "babel-loader": "^7.0.0", + "babel-loader": "^7.1.1", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-function-bind": "^6.22.0", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", "babel-preset-stage-1": "^6.24.1", - "css-loader": "^0.28.3", + "css-loader": "^0.28.4", "i18next-conv": "^3.0.3", - "style-loader": "^0.18.1", - "url-loader": "^0.5.8", + "style-loader": "^0.18.2", + "url-loader": "^0.5.9", "webpack": "^2.6.1" } } diff --git a/client/src/account/API.js b/client/src/account/API.js index f18347a4..23838ea5 100644 --- a/client/src/account/API.js +++ b/client/src/account/API.js @@ -1,7 +1,7 @@ 'use strict'; import React, { Component } from 'react'; -import { translate } from 'react-i18next'; +import { translate, Trans } from 'react-i18next'; import { withPageHelpers, Title } from '../lib/page' import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; import URL from 'url-parse'; @@ -23,7 +23,9 @@ export default class API extends Component { @withAsyncErrorHandler async loadAccessToken() { const response = await axios.get('/rest/access-token'); - this.setState('accessToken', response.data); + this.setState({ + accessToken: response.data + }); } componentDidMount() { @@ -32,7 +34,9 @@ export default class API extends Component { async resetAccessToken() { const response = await axios.post('/rest/access-token-reset'); - this.setState('accessToken', response.data); + this.setState({ + accessToken: response.data + }); } render() { @@ -42,33 +46,36 @@ export default class API extends Component { const serviceUrl = thisUrl.origin + '/'; const accessToken = this.state.accessToken || 'ACCESS_TOKEN'; + let accessTokenMsg; + if (this.state.accessToken) { + accessTokenMsg =
{t('Personal access token') + ': '}{accessToken}
; + } else { + accessTokenMsg =
{t('Access token not yet generated')}
; + } + return (
{t('Sign in')} -
-
-
+
+
+
- { this.state.accessToken ? -
{t('Personal access token:')} {accessToken}
- : -
{t('Access token not yet generated')}
- } + {accessTokenMsg}
-
+

{t('Notes about the API')}

  • - {t('API response is a JSON structure with error and data properties. If the response error has a value set then the request failed.')} + API response is a JSON structure with error and data properties. If the response error has a value set then the request failed.
  • - {t('You need to define proper Content-Type when making a request. You can either use application/x-www-form-urlencoded for normal form data or application/json for a JSON payload. Using multipart/form-data is not supported.')} + You need to define proper Content-Type when making a request. You can either use application/x-www-form-urlencoded for normal form data or application/json for a JSON payload. Using multipart/form-data is not supported.
@@ -183,9 +190,11 @@ export default class API extends Component {

  • access_token – {t('your personal access token')} +
    • start – {t('Start position')} ({t('optional, default 0')})
    • limit – {t('limit emails count in response')} ({t('optional, default 10000')})
    • search – {t('filter by part of email')} ({t('optional, default ""')})
    • +
diff --git a/client/src/account/Account.js b/client/src/account/Account.js index 5c7c05da..282279a0 100644 --- a/client/src/account/Account.js +++ b/client/src/account/Account.js @@ -8,6 +8,7 @@ import { } from '../lib/form'; import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; import passwordValidator from '../../../shared/password-validator'; +import interoperableErrors from '../../../shared/interoperable-errors'; import mailtrainConfig from 'mailtrainConfig'; @translate() @@ -120,6 +121,7 @@ export default class Account extends Component { this.clearFormStatusMessage(); } else { + this.enableForm(); this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.')); } } catch (error) { diff --git a/client/src/account/Forgot.js b/client/src/account/Forgot.js index efa84ef1..7efb91b1 100644 --- a/client/src/account/Forgot.js +++ b/client/src/account/Forgot.js @@ -47,7 +47,7 @@ export default class Forget extends Component { const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/password-reset-send'); if (submitSuccessful) { - this.navigateToWithFlashMessage('/login', 'success', t('If the username / email exists in the system, password reset link will be sent to the registered email.')); + this.navigateToWithFlashMessage('/account/login', 'success', t('If the username / email exists in the system, password reset link will be sent to the registered email.')); } else { this.enableForm(); this.setFormStatusMessage('warning', t('Please enter your username / email and try again.')); diff --git a/client/src/account/Login.js b/client/src/account/Login.js index 24d7f937..4c1f9ad2 100644 --- a/client/src/account/Login.js +++ b/client/src/account/Login.js @@ -9,6 +9,7 @@ import { } from '../lib/form'; import { withErrorHandling } from '../lib/error-handling'; import URL from 'url-parse'; +import interoperableErrors from '../../../shared/interoperable-errors'; import mailtrainConfig from 'mailtrainConfig'; @translate() @@ -61,9 +62,10 @@ export default class Login extends Component { if (submitSuccessful) { const query = new URL(this.props.location.search, true).query; + const nextUrl = query.next || '/'; /* FIXME, once we turn Mailtrain to single-page application, this should become navigateTo */ - window.location = query.next; + window.location = nextUrl; } else { this.setFormStatusMessage('warning', t('Please enter your credentials and try again.')); } diff --git a/client/src/account/Reset.js b/client/src/account/Reset.js index aad088da..1f03a6ea 100644 --- a/client/src/account/Reset.js +++ b/client/src/account/Reset.js @@ -3,12 +3,14 @@ import React, { Component } from 'react'; import { translate } from 'react-i18next'; import { withPageHelpers, Title } from '../lib/page' +import { Link } from 'react-router-dom' import { withForm, Form, Fieldset, FormSendMethod, InputField, ButtonRow, Button } from '../lib/form'; import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; import passwordValidator from '../../../shared/password-validator'; import axios from '../lib/axios'; +import interoperableErrors from '../../../shared/interoperable-errors'; const ResetTokenValidationState = { PENDING: 0, @@ -34,7 +36,7 @@ export default class Account extends Component { } @withAsyncErrorHandler - async loadAccessToken() { + async validateResetToken() { const params = this.props.match.params; const response = await axios.post('/rest/password-reset-validate', { @@ -42,7 +44,9 @@ export default class Account extends Component { resetToken: params.resetToken }); - this.setState('resetTokenValidationState', response.data ? ResetTokenValidationState.VALID : ResetTokenValidationState.INVALID); + this.setState({ + resetTokenValidationState: response.data ? ResetTokenValidationState.VALID : ResetTokenValidationState.INVALID + }); } componentDidMount() { @@ -54,6 +58,8 @@ export default class Account extends Component { password: '', password2: '' }); + + this.validateResetToken(); } localValidateFormValues(state) { @@ -64,7 +70,7 @@ export default class Account extends Component { let passwordMsgs = []; - if (password || currentPassword) { + if (password) { const passwordResults = this.passwordValidator.test(password); passwordMsgs.push(...passwordResults.errors); } @@ -91,6 +97,7 @@ export default class Account extends Component { if (submitSuccessful) { this.navigateToWithFlashMessage('/account/login', 'success', t('Password reset')); } else { + this.enableForm(); this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.')); } } catch (error) { @@ -98,7 +105,7 @@ export default class Account extends Component { this.setFormStatusMessage('danger', {t('Your password cannot be reset.')}{' '} - {t('The reset token has expired.')}{' '}{t('Click here to request a new password reset link.')} + {t('The password reset token has expired.')}{' '}{t('Click here to request a new password reset link.')} ); return; @@ -113,9 +120,17 @@ export default class Account extends Component { if (this.state.resetTokenValidationState === ResetTokenValidationState.PENDING) { return ( -
{t('Validating password reset token ...')}
- ) +

{t('Validating password reset token ...')}

+ ); + } else if (this.state.resetTokenValidationState === ResetTokenValidationState.INVALID) { + return ( +
+ {t('The password cannot be reset')} + +

{t('The password reset token has expired.')}{' '}{t('Click here to request a new password reset link.')}

+
+ ); } else { return ( diff --git a/client/src/account/root.js b/client/src/account/root.js index 7507d5b6..47c60359 100644 --- a/client/src/account/root.js +++ b/client/src/account/root.js @@ -64,7 +64,7 @@ const getStructure = t => { export default function() { ReactDOM.render( -
, +
, document.getElementById('root') ); }; diff --git a/client/src/lib/form.js b/client/src/lib/form.js index ae28e0bb..e55fec29 100644 --- a/client/src/lib/form.js +++ b/client/src/lib/form.js @@ -194,7 +194,7 @@ class CheckBox extends Component { const htmlId = 'form_' + id; return wrapInputInline(id, htmlId, owner, 'checkbox', props.label, props.help, - {console.log(evt); /* FIXME owner.updateFormValue(id, evt.target.value)*/ }}/> + owner.updateFormValue(id, !owner.getFormValue(id))}/> ); } } diff --git a/client/src/users/root.js b/client/src/users/root.js index 0d61f82b..b89e6edf 100644 --- a/client/src/users/root.js +++ b/client/src/users/root.js @@ -20,7 +20,7 @@ const getStructure = t => { render: props => () }; - subPahts.create = { + subPaths.create = { title: t('Create User'), render: props => () }; diff --git a/index.js b/index.js index 305ec2ad..16d83641 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,7 @@ const postfixBounceServer = require('./services/postfix-bounce-server'); const tzupdate = require('./services/tzupdate'); const feedcheck = require('./services/feedcheck'); const dbcheck = require('./lib/dbcheck'); -const tools = require('./lib/tools'); +const senders = require('./lib/senders'); const reportProcessor = require('./lib/report-processor'); const executor = require('./lib/executor'); const privilegeHelpers = require('./lib/privilege-helpers'); @@ -92,11 +92,11 @@ function spawnSenders(callback) { let child = fork(__dirname + '/services/sender.js', []); let pid = child.pid; - tools.workers.add(child); + senders.workers.add(child); child.on('close', (code, signal) => { spawned--; - tools.workers.delete(child); + senders.workers.delete(child); log.error('Child', 'Sender process %s exited with %s', pid, code || signal); // Respawn after 5 seconds setTimeout(() => spawnSender(), 5 * 1000).unref(); diff --git a/lib/db.js b/lib/db.js index 44f17b0f..7c7b42b2 100644 --- a/lib/db.js +++ b/lib/db.js @@ -5,7 +5,7 @@ let mysql = require('mysql'); let redis = require('redis'); let Lock = require('redfour'); let stringifyDate = require('json-stringify-date'); -let tools = require('./tools'); +let senders = require('./senders'); module.exports = mysql.createPool(config.mysql); if (config.redis && config.redis.enabled) { @@ -78,7 +78,7 @@ if (config.redis && config.redis.enabled) { module.exports.clearCache = (key, callback) => { caches.delete(key); - tools.workers.forEach(child => { + senders.workers.forEach(child => { child.send({ cmd: 'db.clearCache', key diff --git a/lib/passport.js b/lib/passport.js index a7afaa38..f67ef1c1 100644 --- a/lib/passport.js +++ b/lib/passport.js @@ -53,11 +53,14 @@ module.exports.restLogout = (req, res) => { module.exports.restLogin = (req, res, next) => { passport.authenticate(config.ldap.enabled ? 'ldap' : 'local', (err, user, info) => { - return next(err); + if (err) { + return next(err); + } if (!user) { return next(new interoperableErrors.IncorrectPasswordError()); } + req.logIn(user, err => { if (err) { return next(err); @@ -79,7 +82,7 @@ module.exports.restLogin = (req, res, next) => { if (config.ldap.enabled && LdapStrategy) { log.info('Using LDAP auth'); module.exports.authMethod = 'ldap'; - module.exports.isAuthMethodLocal = false; + module.exports.isAuthMethodLocal = true; let opts = { server: { diff --git a/lib/senders.js b/lib/senders.js new file mode 100644 index 00000000..c9086bf4 --- /dev/null +++ b/lib/senders.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + workers: new Set() +}; diff --git a/models/settings.js b/models/settings.js index c6031376..668ab93f 100644 --- a/models/settings.js +++ b/models/settings.js @@ -1,7 +1,5 @@ 'use strict'; -'use strict'; - const knex = require('../lib/knex'); const tools = require('../lib/tools'); @@ -15,11 +13,11 @@ async function get(keyOrKeys) { keys = keys.map(key => tools.toDbKey(key)); - const result = await knex('settings').whereIn('key', keys); + const rows = await knex('settings').select(['key', 'value']).whereIn('key', keys); const settings = {}; - for (const key of keys) { - settings[tools.fromDbKey(key)] = result[key]; + for (const row of rows) { + settings[tools.fromDbKey(row.key)] = row.value; } if (!Array.isArray(keyOrKeys)) { diff --git a/models/users.js b/models/users.js index 774ab0fd..1b9c6b6c 100644 --- a/models/users.js +++ b/models/users.js @@ -10,6 +10,8 @@ const dtHelpers = require('../lib/dt-helpers'); const tools = require('../lib/tools-async'); let crypto = require('crypto'); const settings = require('./settings'); +const urllib = require('url'); +const _ = require('../lib/translate')._; const bluebird = require('bluebird'); @@ -23,12 +25,13 @@ const mailerSendMail = bluebird.promisify(mailer.sendMail); const allowedKeys = new Set(['username', 'name', 'email', 'password']); const allowedKeysExternal = new Set(['username']); const ownAccountAllowedKeys = new Set(['name', 'email', 'password']); +const hashKeys = new Set(['username', 'name', 'email']); -const passport = require('../../lib/passport'); +const passport = require('../lib/passport'); function hash(user) { - return hasher.hash(filterObject(user, allowedKeys)); + return hasher.hash(filterObject(user, hashKeys)); } async function _getBy(key, value, extraColumns) { @@ -170,7 +173,7 @@ async function updateWithConsistencyCheck(user, isOwnAccount) { } const existingUserHash = hash(existingUser); - if (existingUserHash != user.originalHash) { + if (existingUserHash !== user.originalHash) { throw new interoperableErrors.ChangedError(); } @@ -201,13 +204,22 @@ async function getByUsername(username) { } async function getByUsernameIfPasswordMatch(username, password) { - const user = await _getBy('username', username, ['password']); + try { + const user = await _getBy('username', username, ['password']); - if (!await bcryptCompare(password, user.password)) { - throw new interoperableErrors.IncorrectPasswordError(); + if (!await bcryptCompare(password, user.password)) { + throw new interoperableErrors.IncorrectPasswordError(); + } + + return user; + + } catch (err) { + if (err instanceof interoperableErrors.NotFoundError) { + throw new interoperableErrors.IncorrectPasswordError(); + } + + throw err; } - - return user; } async function getAccessToken(userId) { @@ -258,7 +270,7 @@ async function sendPasswordReset(usernameOrEmail) { title: 'Mailtrain', username: user.username, name: user.name, - confirmUrl: urllib.resolve(serviceUrl, `/account/reset-link/${encodeURIComponent(user.username)}/${encodeURIComponent(resetToken)}`) + confirmUrl: urllib.resolve(serviceUrl, `/account/reset/${encodeURIComponent(user.username)}/${encodeURIComponent(resetToken)}`) } }); } @@ -273,7 +285,7 @@ async function isPasswordResetTokenValid(username, resetToken) { return !!user; } -async function resetPassword(username, resetToken, password) {R +async function resetPassword(username, resetToken, password) { enforce(passport.isAuthMethodLocal, 'Local user management is required'); await knex.transaction(async tx => { diff --git a/routes/blacklist.js b/routes/blacklist.js index 2f009267..d1897765 100644 --- a/routes/blacklist.js +++ b/routes/blacklist.js @@ -11,7 +11,7 @@ let _ = require('../lib/translate')._; router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } res.setSelectedMenu('blacklist'); next(); diff --git a/routes/campaigns.js b/routes/campaigns.js index 9f721db0..1bead6b3 100644 --- a/routes/campaigns.js +++ b/routes/campaigns.js @@ -24,7 +24,7 @@ let uploads = multer({ router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } res.setSelectedMenu('campaigns'); next(); diff --git a/routes/fields.js b/routes/fields.js index 2ce557da..1db3c8b5 100644 --- a/routes/fields.js +++ b/routes/fields.js @@ -11,7 +11,7 @@ let _ = require('../lib/translate')._; router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } res.setSelectedMenu('lists'); next(); diff --git a/routes/forms.js b/routes/forms.js index ef10bd9a..da3dd5a0 100644 --- a/routes/forms.js +++ b/routes/forms.js @@ -14,7 +14,7 @@ let subscriptions = require('../lib/models/subscriptions'); router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } res.setSelectedMenu('lists'); next(); diff --git a/routes/grapejs.js b/routes/grapejs.js index 38546503..780411fd 100644 --- a/routes/grapejs.js +++ b/routes/grapejs.js @@ -13,7 +13,7 @@ const editorHelpers = require('../lib/editor-helpers') router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } next(); }); diff --git a/routes/lists.js b/routes/lists.js index 4c28bf32..424a647f 100644 --- a/routes/lists.js +++ b/routes/lists.js @@ -48,7 +48,7 @@ let moment = require('moment-timezone'); router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } res.setSelectedMenu('lists'); next(); diff --git a/routes/mosaico.js b/routes/mosaico.js index 2a083af9..adb7a564 100644 --- a/routes/mosaico.js +++ b/routes/mosaico.js @@ -12,7 +12,7 @@ let editorHelpers = require('../lib/editor-helpers'); router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } next(); }); diff --git a/routes/report-templates.js b/routes/report-templates.js index 7fdc4fbc..d954bae5 100644 --- a/routes/report-templates.js +++ b/routes/report-templates.js @@ -18,7 +18,7 @@ const allowedMimeTypes = { router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } res.setSelectedMenu('reports'); next(); diff --git a/routes/reports.js b/routes/reports.js index f1d52e9f..90486a4d 100644 --- a/routes/reports.js +++ b/routes/reports.js @@ -20,7 +20,7 @@ const hbs = require('hbs'); router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } res.setSelectedMenu('reports'); next(); diff --git a/routes/rest/account.js b/routes/rest/account.js index a354ccad..ff6fd9c5 100644 --- a/routes/rest/account.js +++ b/routes/rest/account.js @@ -9,6 +9,7 @@ const router = require('../../lib/router-async').create(); router.getAsync('/account', passport.loggedIn, async (req, res) => { const user = await users.getById(req.user.id); + user.hash = users.hash(user); return res.json(user); }); @@ -20,7 +21,7 @@ router.postAsync('/account', passport.loggedIn, passport.csrfProtection, async ( return res.json(); }); -router.postAsync('/account-validate', passport.loggedIn, async (req, res) => { +router.postAsync('/account-validate', passport.loggedIn, passport.csrfProtection, async (req, res) => { const data = req.body; data.id = req.user.id; @@ -39,20 +40,20 @@ router.postAsync('/access-token-reset', passport.loggedIn, passport.csrfProtecti }); -router.post('/login', passport.restLogin); -router.post('/logout', passport.restLogout); // TODO - this endpoint is currently not in use. It will become relevant once we switch to SPA +router.post('/login', passport.csrfProtection, passport.restLogin); +router.post('/logout', passport.csrfProtection, passport.restLogout); // TODO - this endpoint is currently not in use. It will become relevant once we switch to SPA -router.postAsync('/password-reset-send', async (req, res) => { - await users.sendPasswordReset(req.body.username); +router.postAsync('/password-reset-send', passport.csrfProtection, async (req, res) => { + await users.sendPasswordReset(req.body.usernameOrEmail); return res.json(); }); -router.getAsync('/password-reset-validate', async (req, res) => { +router.postAsync('/password-reset-validate', passport.csrfProtection, async (req, res) => { const isValid = await users.isPasswordResetTokenValid(req.body.username, req.body.resetToken); return res.json(isValid); }) -router.getAsync('/password-reset', async (req, res) => { +router.postAsync('/password-reset', passport.csrfProtection, async (req, res) => { await users.resetPassword(req.body.username, req.body.resetToken, req.body.password); return res.json(); }) diff --git a/routes/rest/namespaces.js b/routes/rest/namespaces.js index 09eb8266..5e28c240 100644 --- a/routes/rest/namespaces.js +++ b/routes/rest/namespaces.js @@ -3,7 +3,6 @@ const passport = require('../../lib/passport'); const _ = require('../../lib/translate')._; const namespaces = require('../../models/namespaces'); -const interoperableErrors = require('../../shared/interoperable-errors'); const router = require('../../lib/router-async').create(); diff --git a/routes/rest/users.js b/routes/rest/users.js index 3f10bf8f..62a102a5 100644 --- a/routes/rest/users.js +++ b/routes/rest/users.js @@ -3,7 +3,6 @@ const passport = require('../../lib/passport'); const _ = require('../../lib/translate')._; const users = require('../../models/users'); -const interoperableErrors = require('../../shared/interoperable-errors'); const router = require('../../lib/router-async').create(); diff --git a/routes/segments.js b/routes/segments.js index 92759598..c87396bb 100644 --- a/routes/segments.js +++ b/routes/segments.js @@ -11,7 +11,7 @@ let _ = require('../lib/translate')._; router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } res.setSelectedMenu('lists'); next(); diff --git a/routes/settings.js b/routes/settings.js index 6ac92446..32977b29 100644 --- a/routes/settings.js +++ b/routes/settings.js @@ -13,6 +13,7 @@ let upload = multer(); let aws = require('aws-sdk'); let util = require('util'); let _ = require('../lib/translate')._; +const senders = require('../lib/senders'); let settings = require('../lib/models/settings'); @@ -21,7 +22,7 @@ let allowedKeys = ['service_url', 'smtp_hostname', 'smtp_port', 'smtp_encryption router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } res.setSelectedMenu('/settings'); next(); @@ -107,7 +108,7 @@ router.post('/update', passport.parseForm, passport.csrfProtection, (req, res) = let storeSettings = () => { if (i >= keys.length) { mailer.update(); - tools.workers.forEach(worker => { + senders.workers.forEach(worker => { worker.send({ reload: true }); diff --git a/routes/templates.js b/routes/templates.js index 3272df52..0b503897 100644 --- a/routes/templates.js +++ b/routes/templates.js @@ -16,7 +16,7 @@ let _ = require('../lib/translate')._; router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } res.setSelectedMenu('templates'); next(); diff --git a/routes/triggers.js b/routes/triggers.js index 6e42aec7..1f412252 100644 --- a/routes/triggers.js +++ b/routes/triggers.js @@ -16,7 +16,7 @@ let util = require('util'); router.all('/*', (req, res, next) => { if (!req.user) { req.flash('danger', _('Need to be logged in to access restricted content')); - return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl)); + return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl)); } res.setSelectedMenu('triggers'); next(); diff --git a/test/e2e/page-objects/user.js b/test/e2e/page-objects/user.js index 736377da..0e09c844 100644 --- a/test/e2e/page-objects/user.js +++ b/test/e2e/page-objects/user.js @@ -7,9 +7,9 @@ module.exports = { url: '/users/login', elementsToWaitFor: ['submitButton'], elements: { - usernameInput: 'form[action="/users/login"] input[name="username"]', - passwordInput: 'form[action="/users/login"] input[name="password"]', - submitButton: 'form[action="/users/login"] [type=submit]' + usernameInput: 'form[action="/account/login"] input[name="username"]', + passwordInput: 'form[action="/account/login"] input[name="password"]', + submitButton: 'form[action="/account/login"] [type=submit]' } }), diff --git a/views/layout.hbs b/views/layout.hbs index 27c6923e..84496256 100644 --- a/views/layout.hbs +++ b/views/layout.hbs @@ -24,7 +24,7 @@ @@ -75,37 +75,47 @@
  • {{title}}
  • {{/if}} {{/each}} + + {{#if admin }} + + {{/if}} +
  • {{#translate}}Wiki{{/translate}}
  • {{#translate}}Blog{{/translate}}
  • - {{#if admin }} - - {{/if}} - - {{#if user }}