From be7da791db49de69d423e977f1f5f5c3117065a8 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sat, 8 Jul 2017 20:32:04 +0200 Subject: [PATCH] LDAP auth seems to work too. Users completely refactored to ReactJS and Knex Initial draft of call context passing (for the time being only in users:remove --- client/src/account/Account.js | 14 +- client/src/account/Login.js | 9 +- client/src/account/root.js | 4 +- lib/client-helpers.js | 4 +- lib/models/users-legacy-REMOVE.js | 346 ------------------------------ lib/passport.js | 5 +- routes/users-legacy-REMOVE.js | 118 ---------- views/users-REMOVE/api.hbs | 219 ------------------- views/users-REMOVE/forgot.hbs | 38 ---- views/users-REMOVE/login.hbs | 43 ---- views/users-REMOVE/reset.hbs | 40 ---- 11 files changed, 24 insertions(+), 816 deletions(-) delete mode 100644 lib/models/users-legacy-REMOVE.js delete mode 100644 routes/users-legacy-REMOVE.js delete mode 100644 views/users-REMOVE/api.hbs delete mode 100644 views/users-REMOVE/forgot.hbs delete mode 100644 views/users-REMOVE/login.hbs delete mode 100644 views/users-REMOVE/reset.hbs diff --git a/client/src/account/Account.js b/client/src/account/Account.js index 282279a0..3948ba93 100644 --- a/client/src/account/Account.js +++ b/client/src/account/Account.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 { withForm, Form, Fieldset, FormSendMethod, InputField, ButtonRow, Button @@ -185,11 +185,15 @@ export default class Account extends Component { ); } else { -
- {t('Account')} + return ( +
+ {t('Account')} -

Account management is not possible because Mailtrain is configured to use externally managed users.

-
+

{t('Account management is not possible because Mailtrain is configured to use externally managed users.')}

+ + {mailtrainConfig.externalPasswordResetLink &&

If you want to change the password, use this link.

} +
+ ); } } } diff --git a/client/src/account/Login.js b/client/src/account/Login.js index 4c1f9ad2..d0b0d7d8 100644 --- a/client/src/account/Login.js +++ b/client/src/account/Login.js @@ -89,6 +89,13 @@ export default class Login extends Component { render() { const t = this.props.t; + let passwordResetLink; + if (mailtrainConfig.isAuthMethodLocal) { + passwordResetLink = {t('Forgot your password?')}; + } else if (mailtrainConfig.externalPasswordResetLink) { + passwordResetLink = {t('Forgot your password?')}; + } + return (
{t('Sign in')} @@ -100,7 +107,7 @@ export default class Login extends Component {
diff --git a/client/src/account/root.js b/client/src/account/root.js index 47c60359..e7e928be 100644 --- a/client/src/account/root.js +++ b/client/src/account/root.js @@ -16,12 +16,12 @@ import mailtrainConfig from 'mailtrainConfig'; const getStructure = t => { const subPaths = { - 'login': { + login: { title: t('Sign in'), link: '/account/login', component: Login, }, - 'api': { + api: { title: t('API'), link: '/account/api', component: API diff --git a/lib/client-helpers.js b/lib/client-helpers.js index b9c6d379..7559f33f 100644 --- a/lib/client-helpers.js +++ b/lib/client-helpers.js @@ -1,11 +1,13 @@ 'use strict'; const passport = require('./passport'); +const config = require('config'); function _getConfig() { return { authMethod: passport.authMethod, - isAuthMethodLocal: passport.isAuthMethodLocal + isAuthMethodLocal: passport.isAuthMethodLocal, + externalPasswordResetLink: config.ldap.passwordresetlink } } diff --git a/lib/models/users-legacy-REMOVE.js b/lib/models/users-legacy-REMOVE.js deleted file mode 100644 index 7c42a0c9..00000000 --- a/lib/models/users-legacy-REMOVE.js +++ /dev/null @@ -1,346 +0,0 @@ -'use strict'; - -let log = require('npmlog'); - -let bcrypt = require('bcrypt-nodejs'); -let db = require('../db'); -let tools = require('../tools'); -let mailer = require('../mailer'); -let settings = require('./settings'); -let crypto = require('crypto'); -let urllib = require('url'); -let _ = require('../translate')._; - -/** - * Fetches user by ID value - * - * @param {Number} id User id - * @param {Function} callback Return an error or an user object - */ -module.exports.get = (id, callback) => { - db.getConnection((err, connection) => { - if (err) { - return callback(err); - } - connection.query('SELECT `id`, `username`, `email`, `access_token` FROM `users` WHERE `id`=? LIMIT 1', [id], (err, rows) => { - connection.release(); - - if (err) { - return callback(err); - } - - if (!rows.length) { - return callback(null, false); - } - - let user = tools.convertKeys(rows[0]); - return callback(null, user); - }); - }); -}; - -module.exports.add = (username, password, email, callback) => { - db.getConnection((err, connection) => { - if (err) { - return callback(err); - } - - connection.query('INSERT INTO `users` (`username`, `password`, `email`, `created`) VALUES (?, ?, ?, NOW())', [username, password, email], (err, result) => { - connection.release(); - - if (err) { - return callback(err); - } - - let id = result && result.insertId; - if (!id) { - return callback(new Error(_('Could not store user row'))); - } - - return callback(null, id); - }); - }); -}; - -/** - * Fetches user by username and password - * - * @param {String} username - * @param {String} password - * @param {Function} callback Return an error or authenticated user - */ -module.exports.authenticate = (username, password, callback) => { - - if (password === '') { - return callback(null, false); - } - - let login = (connection, callback) => { - connection.query('SELECT `id`, `password`, `access_token` FROM `users` WHERE `username`=? OR email=? LIMIT 1', [username, username], (err, rows) => { - if (err) { - return callback(err); - } - - if (!rows.length) { - return callback(null, false); - } - - bcrypt.compare(password, rows[0].password, (err, result) => { - if (err) { - return callback(err); - } - if (!result) { - return callback(null, false); - } - - let user = tools.convertKeys(rows[0]); - return callback(null, { - id: user.id, - username - }); - }); - - }); - }; - - db.getConnection((err, connection) => { - if (err) { - return callback(err); - } - login(connection, (err, user) => { - connection.release(); - callback(err, user); - }); - }); -}; - -/** - * Updates user password - * - * @param {Object} id User ID - * @param {Object} updates - * @param {Function} Return an error or success/fail - */ -module.exports.update = (id, updates, callback) => { - - if (!updates.email) { - return callback(new Error(_('Email Address must be set'))); - } - - let update = (connection, callback) => { - - connection.query('SELECT password FROM users WHERE id=? LIMIT 1', [id], (err, rows) => { - if (err) { - return callback(err); - } - - if (!rows.length) { - return callback(_('Failed to check user data')); - } - - let keys = ['email']; - let values = [updates.email]; - - let finalize = () => { - values.push(id); - connection.query('UPDATE users SET ' + keys.map(key => key + '=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => { - if (err) { - if (err.code === 'ER_DUP_ENTRY') { - err = new Error(_('Can\'t change email as another user with the same email address already exists')); - } - return callback(err); - } - return callback(null, result.affectedRows); - }); - }; - - if (!updates.password && !updates.password2) { - return finalize(); - } - - bcrypt.compare(updates.currentPassword, rows[0].password, (err, result) => { - if (err) { - return callback(err); - } - if (!result) { - return callback(_('Incorrect current password')); - } - - if (!updates.password) { - return callback(new Error(_('New password not set'))); - } - - if (updates.password !== updates.password2) { - return callback(new Error(_('Passwords do not match'))); - } - - bcrypt.hash(updates.password, null, null, (err, hash) => { - if (err) { - return callback(err); - } - - keys.push('password'); - values.push(hash); - - finalize(); - }); - }); - }); - }; - - tools.validateEmail(updates.email, false, err => { - if (err) { - return callback(err); - } - - db.getConnection((err, connection) => { - if (err) { - return callback(err); - } - update(connection, (err, updated) => { - connection.release(); - callback(err, updated); - }); - }); - }); -}; - -module.exports.resetToken = (id, callback) => { - id = Number(id) || 0; - - if (!id) { - return callback(new Error(_('User ID not set'))); - } - - db.getConnection((err, connection) => { - if (err) { - return callback(err); - } - - let token = crypto.randomBytes(20).toString('hex').toLowerCase(); - let query = 'UPDATE users SET `access_token`=? WHERE id=? LIMIT 1'; - let values = [token, id]; - - connection.query(query, values, (err, result) => { - connection.release(); - if (err) { - return callback(err); - } - return callback(null, result.affectedRows); - }); - }); - -}; - - -module.exports.sendReset = (username, callback) => { - username = (username || '').toString().trim(); - - if (!username) { - return callback(new Error(_('Username must be set'))); - } - - db.getConnection((err, connection) => { - if (err) { - return callback(err); - } - connection.query('SELECT id, email, username FROM users WHERE username=? OR email=? LIMIT 1', [username, username], (err, rows) => { - if (err) { - connection.release(); - return callback(err); - } - - if (!rows.length) { - connection.release(); - return callback(null, false); - } - - let resetToken = crypto.randomBytes(16).toString('base64').replace(/[^a-z0-9]/gi, ''); - connection.query('UPDATE users SET reset_token=?, reset_expire=NOW() + INTERVAL 1 HOUR WHERE id=? LIMIT 1', [resetToken, rows[0].id], err => { - connection.release(); - if (err) { - return callback(err); - } - - settings.list(['serviceUrl', 'adminEmail'], (err, configItems) => { - if (err) { - return callback(err); - } - - mailer.sendMail({ - from: { - address: configItems.adminEmail - }, - to: { - address: rows[0].email - }, - subject: _('Mailer password change request') - }, { - html: 'emails/password-reset-html.hbs', - text: 'emails/password-reset-text.hbs', - data: { - title: 'Mailtrain', - username: rows[0].username, - confirmUrl: urllib.resolve(configItems.serviceUrl, '/users/reset') + '?token=' + encodeURIComponent(resetToken) + '&username=' + encodeURIComponent(rows[0].username) - } - }, err => { - if (err) { - log.error('Mail', err); // eslint-disable-line no-console - } - }); - - callback(null, true); - }); - }); - }); - }); -}; - -module.exports.checkResetToken = (username, resetToken, callback) => { - if (!username || !resetToken) { - return callback(new Error(_('Missing username or reset token'))); - } - db.getConnection((err, connection) => { - if (err) { - return callback(err); - } - connection.query('SELECT id FROM users WHERE username=? AND reset_token=? AND reset_expire > NOW() LIMIT 1', [username, resetToken], (err, rows) => { - connection.release(); - if (err) { - return callback(err); - } - return callback(null, rows && rows.length || false); - }); - }); -}; - -module.exports.resetPassword = (data, callback) => { - let updates = tools.convertKeys(data); - - if (!updates.username || !updates.resetToken) { - return callback(new Error(_('Missing username or reset token'))); - } - - if (!updates.password || !updates.password2 || updates.password !== updates.password2) { - return callback(new Error(_('Invalid new password'))); - } - - bcrypt.hash(updates.password, null, null, (err, hash) => { - if (err) { - return callback(err); - } - - db.getConnection((err, connection) => { - if (err) { - return callback(err); - } - connection.query('UPDATE users SET password=?, reset_token=NULL, reset_expire=NULL WHERE username=? AND reset_token=? AND reset_expire > NOW() LIMIT 1', [hash, updates.username, updates.resetToken], (err, result) => { - connection.release(); - if (err) { - return callback(err); - } - return callback(null, result.affectedRows); - }); - }); - }); -}; diff --git a/lib/passport.js b/lib/passport.js index f67ef1c1..9a85a29f 100644 --- a/lib/passport.js +++ b/lib/passport.js @@ -82,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 = true; + module.exports.isAuthMethodLocal = false; let opts = { server: { @@ -93,8 +93,7 @@ if (config.ldap.enabled && LdapStrategy) { filter: config.ldap.filter, attributes: [config.ldap.uidTag, config.ldap.nameTag, 'mail'], scope: 'sub' - }, - uidTag: config.ldap.uidTag + } }; passport.use(new LdapStrategy(opts, nodeifyFunction(async (profile) => { diff --git a/routes/users-legacy-REMOVE.js b/routes/users-legacy-REMOVE.js deleted file mode 100644 index f57a566d..00000000 --- a/routes/users-legacy-REMOVE.js +++ /dev/null @@ -1,118 +0,0 @@ -'use strict'; - -let passport = require('../lib/passport'); -let express = require('express'); -let router = new express.Router(); -let users = require('../lib/models/users-legacy-REMOVE'); -let fields = require('../lib/models/fields'); -let settings = require('../lib/models/settings'); -let _ = require('../lib/translate')._; - -router.get('/logout', (req, res) => passport.logout(req, res)); - -router.post('/login', passport.parseForm, (req, res, next) => passport.login(req, res, next)); -router.get('/login', (req, res) => { - res.render('users/login', { - next: req.query.next - }); -}); - -router.get('/forgot', passport.csrfProtection, (req, res) => { - res.render('users/forgot', { - csrfToken: req.csrfToken() - }); -}); - -router.post('/forgot', passport.parseForm, passport.csrfProtection, (req, res) => { - users.sendReset(req.body.username, err => { - if (err) { - req.flash('danger', err.message || err); - return res.redirect('/users/forgot'); - } else { - req.flash('success', _('An email with password reset instructions has been sent to your email address, if it exists on our system.')); - } - return res.redirect('/users/login'); - }); -}); - -router.get('/reset', passport.csrfProtection, (req, res) => { - users.checkResetToken(req.query.username, req.query.token, (err, status) => { - if (err) { - req.flash('danger', err.message || err); - return res.redirect('/users/login'); - } - - if (!status) { - req.flash('danger', _('Unknown or expired reset token')); - return res.redirect('/users/login'); - } - - res.render('users/reset', { - csrfToken: req.csrfToken(), - username: req.query.username, - resetToken: req.query.token - }); - }); -}); - -router.post('/reset', passport.parseForm, passport.csrfProtection, (req, res) => { - users.resetPassword(req.body, (err, status) => { - if (err) { - req.flash('danger', err.message || err); - return res.redirect('/users/reset?username=' + encodeURIComponent(req.body.username) + '&token=' + encodeURIComponent(req.body['reset-token'])); - } else if (!status) { - req.flash('danger', _('Unknown or expired reset token')); - } else { - req.flash('success', _('Your password has been changed successfully')); - } - - return res.redirect('/users/login'); - }); -}); - -router.all('/api', (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)); - } - next(); -}); - -router.get('/api', passport.csrfProtection, (req, res, next) => { - users.get(req.user.id, (err, user) => { - if (err) { - return next(err); - } - if (!user) { - return next(new Error(_('User data not found'))); - } - settings.list(['serviceUrl'], (err, configItems) => { - if (err) { - return next(err); - } - user.serviceUrl = configItems.serviceUrl; - user.csrfToken = req.csrfToken(); - user.allowedTypes = Object.keys(fields.types).map(key => ({ - type: key, - description: fields.types[key] - })); - res.render('users/api', user); - }); - }); - -}); - -router.post('/api/reset-token', passport.parseForm, passport.csrfProtection, (req, res) => { - users.resetToken(Number(req.user.id), (err, success) => { - if (err) { - req.flash('danger', err.message || err); - } else if (success) { - req.flash('success', _('Access token updated')); - } else { - req.flash('info', _('Access token not updated')); - } - return res.redirect('/users/api'); - }); -}); - -module.exports = router; diff --git a/views/users-REMOVE/api.hbs b/views/users-REMOVE/api.hbs deleted file mode 100644 index 1a322032..00000000 --- a/views/users-REMOVE/api.hbs +++ /dev/null @@ -1,219 +0,0 @@ - - -

{{#translate}}API{{/translate}}

- -
- -
-
-
-
- - -
-
- {{#if accessToken}} - {{#translate}}Personal access token:{{/translate}} {{accessToken}} - {{else}} - {{#translate}}Access token not yet generated{{/translate}} - {{/if}} -
-
- -
- -

{{#translate}}Notes about the API{{/translate}}

- - -
- -

POST /api/subscribe/:listId – {{#translate}}Add subscription{{/translate}}

- -

- {{#translate}}This API call either inserts a new subscription or updates existing. Fields not included are left as is, so if you update only LAST_NAME value, then FIRST_NAME is kept untouched for an existing subscription.{{/translate}} -

- -

- GET {{#translate}}arguments{{/translate}} -

- - -

- POST {{#translate}}arguments{{/translate}} -

- - -

- {{#translate}}Additional POST arguments{{/translate}}: -

- - - -

- {{#translate}}Example{{/translate}} -

- -
curl -XPOST {{serviceUrl}}api/subscribe/B16uVTdW?access_token={{accessToken}} \
---data 'EMAIL=test@example.com&MERGE_CHECKBOX=yes&REQUIRE_CONFIRMATION=yes'
- -

POST /api/unsubscribe/:listId – {{#translate}}Remove subscription{{/translate}}

- -

- {{#translate}}This API call marks a subscription as unsubscribed{{/translate}} -

- -

- GET {{#translate}}arguments{{/translate}} -

- - -

- POST {{#translate}}arguments{{/translate}} -

- - -

- {{#translate}}Example{{/translate}} -

- -
curl -XPOST {{serviceUrl}}api/unsubscribe/B16uVTdW?access_token={{accessToken}} \
---data 'EMAIL=test@example.com'
- -

POST /api/delete/:listId – {{#translate}}Delete subscription{{/translate}}

- -

- {{#translate}}This API call deletes a subscription{{/translate}} -

- -

- GET {{#translate}}arguments{{/translate}} -

- - -

- POST {{#translate}}arguments{{/translate}} -

- - -

- {{#translate}}Example{{/translate}} -

- -
curl -XPOST {{serviceUrl}}api/delete/B16uVTdW?access_token={{accessToken}} \
---data 'EMAIL=test@example.com'
- -

GET /api/blacklist/get – {{#translate}}Get list of blacklisted emails{{/translate}}

- -

- {{#translate}}This API call get list of blacklisted emails.{{/translate}} -

- -

- GET {{#translate}}arguments{{/translate}} -

- - -

- {{#translate}}Example{{/translate}} -

- -
curl -XGET '{{serviceUrl}}api/blacklist/get?access_token={{accessToken}}&limit=10&start=10&search=gmail' 
- -

POST /api/blacklist/add – {{#translate}}Add email to blacklist{{/translate}}

- -

- {{#translate}}This API call either add emails to blacklist{{/translate}} -

- -

- GET {{#translate}}arguments{{/translate}} -

- - -

- POST {{#translate}}arguments{{/translate}} -

- - -

- {{#translate}}Example{{/translate}} -

- -
curl -XPOST '{{serviceUrl}}api/blacklist/add?access_token={{accessToken}}' \
---data 'EMAIL=test@example.com&'
- -

POST /api/blacklist/delete – {{#translate}}Delete email from blacklist{{/translate}}

- -

- {{#translate}}This API call either delete emails from blacklist{{/translate}} -

- -

- GET {{#translate}}arguments{{/translate}} -

- - -

- POST {{#translate}}arguments{{/translate}} -

- - -

- {{#translate}}Example{{/translate}} -

- -
curl -XPOST '{{serviceUrl}}api/blacklist/delete?access_token={{accessToken}}' \
---data 'EMAIL=test@example.com&'
diff --git a/views/users-REMOVE/forgot.hbs b/views/users-REMOVE/forgot.hbs deleted file mode 100644 index 492a55f8..00000000 --- a/views/users-REMOVE/forgot.hbs +++ /dev/null @@ -1,38 +0,0 @@ - - -

{{#translate}}Reset your password?{{/translate}}

- -
- -{{#if ldap.enabled}} -

- {{#translate}}Accounts are managed through LDAP.{{/translate}} -
-
- {{#translate}}Reset Password{{/translate}} -

-{{else}} -

{{#translate}}Please provide the username or email address that you used when you signed up for your Mailtrain account.{{/translate}}

-

{{#translate}}We will send you an email that will allow you to reset your password.{{/translate}}

- -
- - - -
- -
- -
-
-
-
- -
-
-
-{{/if}} diff --git a/views/users-REMOVE/login.hbs b/views/users-REMOVE/login.hbs deleted file mode 100644 index be5af8c9..00000000 --- a/views/users-REMOVE/login.hbs +++ /dev/null @@ -1,43 +0,0 @@ - - -

{{#translate}}Sign in{{/translate}}

- -
- -
- -
- -
- -
-
-
- -
- -
-
-
-
-
- -
-
-
-
-
- {{#translate}}or{{/translate}} - {{#if ldap.enabled}} - {{#translate}}Forgot password?{{/translate}} - {{else}} - {{#translate}}Forgot password?{{/translate}} - {{/if}} -
-
-
diff --git a/views/users-REMOVE/reset.hbs b/views/users-REMOVE/reset.hbs deleted file mode 100644 index 20c5e41d..00000000 --- a/views/users-REMOVE/reset.hbs +++ /dev/null @@ -1,40 +0,0 @@ - - -

{{#translate}}Choose your new password{{/translate}}

- -
- -
- - - - - -

- {{#translate}}Please enter a new password.{{/translate}} -

- -
- -
- -
-
-
- -
- -
-
- -
-
- -
-
- -