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 {
- {mailtrainConfig.isAuthMethodLocal && {t('Forgot your password?')}}
+ {passwordResetLink}
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}}Home{{/translate}}
- {{#translate}}API{{/translate}}
-
-
-{{#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}}
-
-
-
- {{#translate}}API response is a JSON structure with error
and data
properties. If the response error
has a value set then the request failed.{{/translate}}
-
-
- {{#translate}}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.{{/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}}
-
-
- access_token – {{#translate}}your personal access token{{/translate}}
-
-
-
- POST {{#translate}}arguments{{/translate}}
-
-
- EMAIL – {{#translate}}subscriber's email address{{/translate}} ({{#translate}}required{{/translate}} )
- FIRST_NAME – {{#translate}}subscriber's first name{{/translate}}
- LAST_NAME – {{#translate}}subscriber's last name{{/translate}}
- TIMEZONE – {{#translate}}subscriber's timezone (eg. "Europe/Tallinn", "PST" or "UTC"). If not set defaults to "UTC"{{/translate}}
- MERGE_TAG_VALUE – {{#translate}}custom field value. Use yes/no for option group values (checkboxes, radios, drop downs){{/translate}}
-
-
-
- {{#translate}}Additional POST arguments{{/translate}}:
-
-
-
-
- FORCE_SUBSCRIBE – {{#translate}}set to "yes" if you want to make sure the email is marked as subscribed even if it was previously marked as unsubscribed. If the email was already unsubscribed/blocked then subscription status is not changed{{/translate}}
- by default.
-
-
- REQUIRE_CONFIRMATION – {{#translate}}set to "yes" if you want to send confirmation email to the subscriber before actually marking as subscribed{{/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}}
-
-
- access_token – {{#translate}}your personal access token{{/translate}}
-
-
-
- POST {{#translate}}arguments{{/translate}}
-
-
- EMAIL – {{#translate}}subscriber's email address{{/translate}} ({{#translate}}required{{/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}}
-
-
- access_token – {{#translate}}your personal access token{{/translate}}
-
-
-
- POST {{#translate}}arguments{{/translate}}
-
-
- EMAIL – {{#translate}}subscriber's email address{{/translate}} ({{#translate}}required{{/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}}
-
-
- access_token – {{#translate}}your personal access token{{/translate}}
- start – {{#translate}}Start position{{/translate}} ({{#translate}}optional, default 0{{/translate}} )
- limit – {{#translate}}limit emails count in response{{/translate}} ({{#translate}}optional, default 10000{{/translate}} )
- search – {{#translate}}filter by part of email{{/translate}} ({{#translate}}optional, default ''{{/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}}
-
-
- access_token – {{#translate}}your personal access token{{/translate}}
-
-
-
- POST {{#translate}}arguments{{/translate}}
-
-
- EMAIL – {{#translate}}email address{{/translate}} ({{#translate}}required{{/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}}
-
-
- access_token – {{#translate}}your personal access token{{/translate}}
-
-
-
- POST {{#translate}}arguments{{/translate}}
-
-
- EMAIL – {{#translate}}email address{{/translate}} ({{#translate}}required{{/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}}Home{{/translate}}
- {{#translate}}Sign in{{/translate}}
- {{#translate}}Password Reset{{/translate}}
-
-
-{{#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}}Home{{/translate}}
- {{#translate}}Sign in{{/translate}}
-
-
-{{#translate}}Sign in{{/translate}}
-
-
-
-
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}}Home{{/translate}}
- {{#translate}}Sign in{{/translate}}
- {{#translate}}Password Reset{{/translate}}
-
-
-{{#translate}}Choose your new password{{/translate}}
-
-
-
-