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
This commit is contained in:
parent
9758b4b104
commit
be7da791db
11 changed files with 24 additions and 816 deletions
|
@ -1,7 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { translate } from 'react-i18next';
|
import { translate, Trans } from 'react-i18next';
|
||||||
import { withPageHelpers, Title } from '../lib/page'
|
import { withPageHelpers, Title } from '../lib/page'
|
||||||
import {
|
import {
|
||||||
withForm, Form, Fieldset, FormSendMethod, InputField, ButtonRow, Button
|
withForm, Form, Fieldset, FormSendMethod, InputField, ButtonRow, Button
|
||||||
|
@ -185,11 +185,15 @@ export default class Account extends Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
<div>
|
return (
|
||||||
<Title>{t('Account')}</Title>
|
<div>
|
||||||
|
<Title>{t('Account')}</Title>
|
||||||
|
|
||||||
<p>Account management is not possible because Mailtrain is configured to use externally managed users.</p>
|
<p>{t('Account management is not possible because Mailtrain is configured to use externally managed users.')}</p>
|
||||||
</div>
|
|
||||||
|
{mailtrainConfig.externalPasswordResetLink && <p><Trans>If you want to change the password, use <a href={mailtrainConfig.externalPasswordResetLink}>this link</a>.</Trans></p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,13 @@ export default class Login extends Component {
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
|
|
||||||
|
let passwordResetLink;
|
||||||
|
if (mailtrainConfig.isAuthMethodLocal) {
|
||||||
|
passwordResetLink = <Link to={`/account/forgot/${this.getFormValue('username')}`}>{t('Forgot your password?')}</Link>;
|
||||||
|
} else if (mailtrainConfig.externalPasswordResetLink) {
|
||||||
|
passwordResetLink = <a href={mailtrainConfig.externalPasswordResetLink}>{t('Forgot your password?')}</a>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Title>{t('Sign in')}</Title>
|
<Title>{t('Sign in')}</Title>
|
||||||
|
@ -100,7 +107,7 @@ export default class Login extends Component {
|
||||||
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Sign in')}/>
|
<Button type="submit" className="btn-primary" icon="ok" label={t('Sign in')}/>
|
||||||
{mailtrainConfig.isAuthMethodLocal && <Link to={`/account/forgot/${this.getFormValue('username')}`}>{t('Forgot your password?')}</Link>}
|
{passwordResetLink}
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,12 +16,12 @@ import mailtrainConfig from 'mailtrainConfig';
|
||||||
|
|
||||||
const getStructure = t => {
|
const getStructure = t => {
|
||||||
const subPaths = {
|
const subPaths = {
|
||||||
'login': {
|
login: {
|
||||||
title: t('Sign in'),
|
title: t('Sign in'),
|
||||||
link: '/account/login',
|
link: '/account/login',
|
||||||
component: Login,
|
component: Login,
|
||||||
},
|
},
|
||||||
'api': {
|
api: {
|
||||||
title: t('API'),
|
title: t('API'),
|
||||||
link: '/account/api',
|
link: '/account/api',
|
||||||
component: API
|
component: API
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const passport = require('./passport');
|
const passport = require('./passport');
|
||||||
|
const config = require('config');
|
||||||
|
|
||||||
function _getConfig() {
|
function _getConfig() {
|
||||||
return {
|
return {
|
||||||
authMethod: passport.authMethod,
|
authMethod: passport.authMethod,
|
||||||
isAuthMethodLocal: passport.isAuthMethodLocal
|
isAuthMethodLocal: passport.isAuthMethodLocal,
|
||||||
|
externalPasswordResetLink: config.ldap.passwordresetlink
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -82,7 +82,7 @@ module.exports.restLogin = (req, res, next) => {
|
||||||
if (config.ldap.enabled && LdapStrategy) {
|
if (config.ldap.enabled && LdapStrategy) {
|
||||||
log.info('Using LDAP auth');
|
log.info('Using LDAP auth');
|
||||||
module.exports.authMethod = 'ldap';
|
module.exports.authMethod = 'ldap';
|
||||||
module.exports.isAuthMethodLocal = true;
|
module.exports.isAuthMethodLocal = false;
|
||||||
|
|
||||||
let opts = {
|
let opts = {
|
||||||
server: {
|
server: {
|
||||||
|
@ -93,8 +93,7 @@ if (config.ldap.enabled && LdapStrategy) {
|
||||||
filter: config.ldap.filter,
|
filter: config.ldap.filter,
|
||||||
attributes: [config.ldap.uidTag, config.ldap.nameTag, 'mail'],
|
attributes: [config.ldap.uidTag, config.ldap.nameTag, 'mail'],
|
||||||
scope: 'sub'
|
scope: 'sub'
|
||||||
},
|
}
|
||||||
uidTag: config.ldap.uidTag
|
|
||||||
};
|
};
|
||||||
|
|
||||||
passport.use(new LdapStrategy(opts, nodeifyFunction(async (profile) => {
|
passport.use(new LdapStrategy(opts, nodeifyFunction(async (profile) => {
|
||||||
|
|
|
@ -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;
|
|
|
@ -1,219 +0,0 @@
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li><a href="/">{{#translate}}Home{{/translate}}</a></li>
|
|
||||||
<li class="active">{{#translate}}API{{/translate}}</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h2>{{#translate}}API{{/translate}}</h2>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-body">
|
|
||||||
<div class="pull-right">
|
|
||||||
<form class="form-horizontal confirm-submit" {{#if accessToken}} data-confirm-message="{{#translate}}Are you sure? Resetting would invalidate the currently existing token.{{/translate}}" {{else}} data-confirm-message="{{#translate}}Are you sure?{{/translate}}" {{/if}} method="post" action="/users/api/reset-token">
|
|
||||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
|
||||||
<button type="submit" class="btn btn-info"><span class="glyphicon glyphicon-retweet" aria-hidden="true"></span>
|
|
||||||
{{#if accessToken}}
|
|
||||||
{{#translate}}Reset Access Token{{/translate}}
|
|
||||||
{{else}}
|
|
||||||
{{#translate}}Generate Access Token{{/translate}}
|
|
||||||
{{/if}}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{{#if accessToken}}
|
|
||||||
{{#translate}}Personal access token:{{/translate}} <code>{{accessToken}}</code>
|
|
||||||
{{else}}
|
|
||||||
{{#translate}}Access token not yet generated{{/translate}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="well">
|
|
||||||
|
|
||||||
<h3>{{#translate}}Notes about the API{{/translate}}</h3>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
{{#translate}}API response is a JSON structure with <code>error</code> and <code>data</code> properties. If the response <code>error</code> has a value set then the request failed.{{/translate}}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{{#translate}}You need to define proper <code>Content-Type</code> when making a request. You can either use <code>application/x-www-form-urlencoded</code> for normal form data or <code>application/json</code> for a JSON payload. Using <code>multipart/form-data</code> is not supported.{{/translate}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>POST /api/subscribe/:listId – {{#translate}}Add subscription{{/translate}}</h3>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{#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}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>GET</strong> {{#translate}}arguments{{/translate}}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>access_token</strong> – {{#translate}}your personal access token{{/translate}}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>POST</strong> {{#translate}}arguments{{/translate}}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>EMAIL</strong> – {{#translate}}subscriber's email address{{/translate}} (<em>{{#translate}}required{{/translate}}</em>)</li>
|
|
||||||
<li><strong>FIRST_NAME</strong> – {{#translate}}subscriber's first name{{/translate}}</li>
|
|
||||||
<li><strong>LAST_NAME</strong> – {{#translate}}subscriber's last name{{/translate}}</li>
|
|
||||||
<li><strong>TIMEZONE</strong> – {{#translate}}subscriber's timezone (eg. "Europe/Tallinn", "PST" or "UTC"). If not set defaults to "UTC"{{/translate}}</li>
|
|
||||||
<li><strong>MERGE_TAG_VALUE</strong> – {{#translate}}custom field value. Use yes/no for option group values (checkboxes, radios, drop downs){{/translate}}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{#translate}}Additional POST arguments{{/translate}}:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<strong>FORCE_SUBSCRIBE</strong> – {{#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.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>REQUIRE_CONFIRMATION</strong> – {{#translate}}set to "yes" if you want to send confirmation email to the subscriber before actually marking as subscribed{{/translate}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>{{#translate}}Example{{/translate}}</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<pre>curl -XPOST {{serviceUrl}}api/subscribe/B16uVTdW?access_token={{accessToken}} \
|
|
||||||
--data 'EMAIL=test@example.com&MERGE_CHECKBOX=yes&REQUIRE_CONFIRMATION=yes'</pre>
|
|
||||||
|
|
||||||
<h3>POST /api/unsubscribe/:listId – {{#translate}}Remove subscription{{/translate}}</h3>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{#translate}}This API call marks a subscription as unsubscribed{{/translate}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>GET</strong> {{#translate}}arguments{{/translate}}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>access_token</strong> – {{#translate}}your personal access token{{/translate}}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>POST</strong> {{#translate}}arguments{{/translate}}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>EMAIL</strong> – {{#translate}}subscriber's email address{{/translate}} (<em>{{#translate}}required{{/translate}}</em>)
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>{{#translate}}Example{{/translate}}</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<pre>curl -XPOST {{serviceUrl}}api/unsubscribe/B16uVTdW?access_token={{accessToken}} \
|
|
||||||
--data 'EMAIL=test@example.com'</pre>
|
|
||||||
|
|
||||||
<h3>POST /api/delete/:listId – {{#translate}}Delete subscription{{/translate}}</h3>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{#translate}}This API call deletes a subscription{{/translate}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>GET</strong> {{#translate}}arguments{{/translate}}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>access_token</strong> – {{#translate}}your personal access token{{/translate}}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>POST</strong> {{#translate}}arguments{{/translate}}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>EMAIL</strong> – {{#translate}}subscriber's email address{{/translate}} (<em>{{#translate}}required{{/translate}}</em>)
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>{{#translate}}Example{{/translate}}</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<pre>curl -XPOST {{serviceUrl}}api/delete/B16uVTdW?access_token={{accessToken}} \
|
|
||||||
--data 'EMAIL=test@example.com'</pre>
|
|
||||||
|
|
||||||
<h3>GET /api/blacklist/get – {{#translate}}Get list of blacklisted emails{{/translate}}</h3>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{#translate}}This API call get list of blacklisted emails.{{/translate}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>GET</strong> {{#translate}}arguments{{/translate}}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>access_token</strong> – {{#translate}}your personal access token{{/translate}}
|
|
||||||
<li><strong>start</strong> – {{#translate}}Start position{{/translate}} (<em>{{#translate}}optional, default 0{{/translate}}</em>)</li>
|
|
||||||
<li><strong>limit</strong> – {{#translate}}limit emails count in response{{/translate}} (<em>{{#translate}}optional, default 10000{{/translate}}</em>)</li>
|
|
||||||
<li><strong>search</strong> – {{#translate}}filter by part of email{{/translate}} (<em>{{#translate}}optional, default ''{{/translate}}</em>)</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>{{#translate}}Example{{/translate}}</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<pre>curl -XGET '{{serviceUrl}}api/blacklist/get?access_token={{accessToken}}&limit=10&start=10&search=gmail' </pre>
|
|
||||||
|
|
||||||
<h3>POST /api/blacklist/add – {{#translate}}Add email to blacklist{{/translate}}</h3>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{#translate}}This API call either add emails to blacklist{{/translate}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>GET</strong> {{#translate}}arguments{{/translate}}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>access_token</strong> – {{#translate}}your personal access token{{/translate}}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>POST</strong> {{#translate}}arguments{{/translate}}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>EMAIL</strong> – {{#translate}}email address{{/translate}} (<em>{{#translate}}required{{/translate}}</em>)</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>{{#translate}}Example{{/translate}}</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<pre>curl -XPOST '{{serviceUrl}}api/blacklist/add?access_token={{accessToken}}' \
|
|
||||||
--data 'EMAIL=test@example.com&'</pre>
|
|
||||||
|
|
||||||
<h3>POST /api/blacklist/delete – {{#translate}}Delete email from blacklist{{/translate}}</h3>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{#translate}}This API call either delete emails from blacklist{{/translate}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>GET</strong> {{#translate}}arguments{{/translate}}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>access_token</strong> – {{#translate}}your personal access token{{/translate}}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>POST</strong> {{#translate}}arguments{{/translate}}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>EMAIL</strong> – {{#translate}}email address{{/translate}} (<em>{{#translate}}required{{/translate}}</em>)</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>{{#translate}}Example{{/translate}}</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<pre>curl -XPOST '{{serviceUrl}}api/blacklist/delete?access_token={{accessToken}}' \
|
|
||||||
--data 'EMAIL=test@example.com&'</pre>
|
|
|
@ -1,38 +0,0 @@
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li><a href="/">{{#translate}}Home{{/translate}}</a></li>
|
|
||||||
<li><a href="/users/login">{{#translate}}Sign in{{/translate}}</a></li>
|
|
||||||
<li class="active">{{#translate}}Password Reset{{/translate}}</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h2>{{#translate}}Reset your password?{{/translate}}</h2>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
{{#if ldap.enabled}}
|
|
||||||
<p>
|
|
||||||
{{#translate}}Accounts are managed through LDAP.{{/translate}}
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
<a href="{{ldap.passwordresetlink}}">{{#translate}}Reset Password{{/translate}}</a>
|
|
||||||
</p>
|
|
||||||
{{else}}
|
|
||||||
<p>{{#translate}}Please provide the username or email address that you used when you signed up for your Mailtrain account.{{/translate}}</p>
|
|
||||||
<p>{{#translate}}We will send you an email that will allow you to reset your password.{{/translate}}</p>
|
|
||||||
|
|
||||||
<form class="form-horizontal" role="login" method="post" action="/users/forgot">
|
|
||||||
|
|
||||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="usernameMain" class="col-sm-2 control-label">{{#translate}}Username{{/translate}}</label>
|
|
||||||
<div class="col-xs-4">
|
|
||||||
<input type="text" class="form-control" name="username" id="usernameMain" placeholder="{{#translate}}Username or email address{{/translate}}" autofocus required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-offset-2 col-xs-4">
|
|
||||||
<button type="submit" class="btn btn-primary">{{#translate}}Send verification email{{/translate}}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{{/if}}
|
|
|
@ -1,43 +0,0 @@
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li><a href="/">{{#translate}}Home{{/translate}}</a></li>
|
|
||||||
<li class="active">{{#translate}}Sign in{{/translate}}</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h2>{{#translate}}Sign in{{/translate}}</h2>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<form class="form-horizontal" role="login" method="post" action="/users/login">
|
|
||||||
<input type="hidden" name="next" value="{{next}}" />
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="usernameMain" class="col-sm-2 control-label">{{#translate}}Username{{/translate}}</label>
|
|
||||||
<div class="col-xs-4">
|
|
||||||
<input type="text" class="form-control" name="username" id="usernameMain" placeholder="{{#translate}}Username{{/translate}}" autofocus required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="passwordMain" class="col-sm-2 control-label">{{#translate}}Password{{/translate}}</label>
|
|
||||||
<div class="col-xs-4">
|
|
||||||
<input type="password" class="form-control" name="password" id="passwordMain" placeholder="{{#translate}}Password{{/translate}}" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-offset-2 col-xs-4">
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="remember"> {{#translate}}Remember me{{/translate}}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-offset-2 col-xs-4">
|
|
||||||
<button type="submit" class="btn btn-primary">{{#translate}}Sign in{{/translate}}</button> {{#translate}}or{{/translate}}
|
|
||||||
{{#if ldap.enabled}}
|
|
||||||
<a href="{{ldap.passwordresetlink}}">{{#translate}}Forgot password?{{/translate}}</a>
|
|
||||||
{{else}}
|
|
||||||
<a href="/users/forgot">{{#translate}}Forgot password?{{/translate}}</a>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
|
@ -1,40 +0,0 @@
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li><a href="/">{{#translate}}Home{{/translate}}</a></li>
|
|
||||||
<li><a href="/users/login">{{#translate}}Sign in{{/translate}}</a></li>
|
|
||||||
<li class="active">{{#translate}}Password Reset{{/translate}}</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h2>{{#translate}}Choose your new password{{/translate}}</h2>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<form class="form-horizontal" method="post" action="/users/reset">
|
|
||||||
|
|
||||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
|
||||||
<input type="hidden" name="username" value="{{username}}">
|
|
||||||
<input type="hidden" name="reset-token" value="{{resetToken}}">
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{#translate}}Please enter a new password.{{/translate}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password" class="col-sm-2 control-label">{{#translate}}New Password{{/translate}}</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<input type="password" class="form-control" name="password" id="password" placeholder="{{#translate}}New Password{{/translate}}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password2" class="col-sm-2 control-label">{{#translate}}Confirm Password{{/translate}}</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<input type="password" class="form-control" name="password2" id="password2" placeholder="{{#translate}}Confirm New Password{{/translate}}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
|
||||||
<button type="submit" class="btn btn-primary">{{#translate}}Reset Password{{/translate}}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
Loading…
Add table
Add a link
Reference in a new issue