All about user login
Not runnable at the moment
This commit is contained in:
parent
fbb8f5799e
commit
d79bbad575
49 changed files with 1554 additions and 686 deletions
34
app.js
34
app.js
|
@ -21,7 +21,6 @@ const passport = require('./lib/passport');
|
|||
const tools = require('./lib/tools');
|
||||
|
||||
const routes = require('./routes/index');
|
||||
const usersOld = require('./routes/users-legacy');
|
||||
const lists = require('./routes/lists');
|
||||
const settings = require('./routes/settings');
|
||||
const settingsModel = require('./lib/models/settings');
|
||||
|
@ -43,9 +42,13 @@ const mosaico = require('./routes/mosaico');
|
|||
const reports = require('./routes/reports');
|
||||
const reportsTemplates = require('./routes/report-templates');
|
||||
|
||||
const namespaces = require('./routes/namespaces');
|
||||
const users = require('./routes/users');
|
||||
const account = require('./routes/account');
|
||||
const namespaces = require('./routes/rest/namespaces');
|
||||
const users = require('./routes/rest/users');
|
||||
const account = require('./routes/rest/account');
|
||||
|
||||
const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
|
||||
const usersLegacyIntegration = require('./routes/users-legacy-integration');
|
||||
const accountLegacyIntegration = require('./routes/account-legacy-integration');
|
||||
|
||||
const interoperableErrors = require('./shared/interoperable-errors');
|
||||
|
||||
|
@ -168,6 +171,7 @@ passport.setup(app);
|
|||
app.use((req, res, next) => {
|
||||
res.locals.flash = req.flash.bind(req);
|
||||
res.locals.user = req.user;
|
||||
res.locals.admin = req.user && req.user.id == 1; // FIXME, this should verify the admin privileges and set this accordingly
|
||||
res.locals.ldap = {
|
||||
enabled: config.ldap.enabled,
|
||||
passwordresetlink: config.ldap.passwordresetlink
|
||||
|
@ -209,7 +213,6 @@ app.use((req, res, next) => {
|
|||
});
|
||||
|
||||
app.use('/', routes);
|
||||
app.use('/users', usersOld);
|
||||
app.use('/lists', lists);
|
||||
app.use('/templates', templates);
|
||||
app.use('/campaigns', campaigns);
|
||||
|
@ -228,9 +231,24 @@ app.use('/editorapi', editorapi);
|
|||
app.use('/grapejs', grapejs);
|
||||
app.use('/mosaico', mosaico);
|
||||
|
||||
app.use('/namespaces', namespaces);
|
||||
app.use('/users', users);
|
||||
app.use('/account', account);
|
||||
/* FIXME - this should be removed once we bind the ReactJS client to / */
|
||||
app.use('/users', usersLegacyIntegration);
|
||||
app.use('/namespaces', namespacesLegacyIntegration);
|
||||
app.use('/account', accountLegacyIntegration);
|
||||
/* ------------------------------------------------------------------- */
|
||||
|
||||
|
||||
app.all('/rest/*', (req, res, next) => {
|
||||
console.log('njr');
|
||||
req.needsJSONResponse = true;
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/rest', namespaces);
|
||||
app.use('/rest', users);
|
||||
app.use('/rest', account);
|
||||
|
||||
|
||||
|
||||
if (config.reports && config.reports.enabled === true) {
|
||||
app.use('/reports', reports);
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
"react": "^15.5.4",
|
||||
"react-dom": "^15.5.4",
|
||||
"react-i18next": "^4.1.0",
|
||||
"react-router-dom": "^4.1.1"
|
||||
"react-router-dom": "^4.1.1",
|
||||
"url-parse": "^1.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
|
|
254
client/src/account/API.js
Normal file
254
client/src/account/API.js
Normal file
|
@ -0,0 +1,254 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { withPageHelpers, Title } from '../lib/page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import URL from 'url-parse';
|
||||
import axios from '../lib/axios';
|
||||
import { Button } from '../lib/bootstrap-components';
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
export default class API extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
accessToken: null
|
||||
};
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadAccessToken() {
|
||||
const response = await axios.get('/rest/access-token');
|
||||
this.setState('accessToken', response.data);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadAccessToken();
|
||||
}
|
||||
|
||||
async resetAccessToken() {
|
||||
const response = await axios.post('/rest/access-token-reset');
|
||||
this.setState('accessToken', response.data);
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const thisUrl = new URL();
|
||||
const serviceUrl = thisUrl.origin + '/';
|
||||
const accessToken = this.state.accessToken || 'ACCESS_TOKEN';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Sign in')}</Title>
|
||||
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div class="pull-right">
|
||||
<Button label={this.state.accessToken ? t('Reset Access Token') : t('Generate Access Token')} icon="retweet" className="btn-info" onClickAsync={::this.resetAccessToken} />
|
||||
</div>
|
||||
{ this.state.accessToken ?
|
||||
<div>{t('Personal access token:')} <code>{accessToken}</code></div>
|
||||
:
|
||||
<div>{t('Access token not yet generated')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="well">
|
||||
<h3>{t('Notes about the API')}</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
{t('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.')}
|
||||
</li>
|
||||
<li>
|
||||
{t('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.')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>POST /api/subscribe/:listId – {t('Add subscription')}</h3>
|
||||
|
||||
<p>
|
||||
{t('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.')}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>GET</strong> {t('arguments')}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>access_token</strong> – {t('your personal access token')}</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>POST</strong> {t('arguments')}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>EMAIL</strong> – {t('subscriber\'s email address')} (<em>{t('required')}</em>)</li>
|
||||
<li><strong>FIRST_NAME</strong> – {t('subscriber\'s first name')}</li>
|
||||
<li><strong>LAST_NAME</strong> – {t('subscriber\'s last name')}</li>
|
||||
<li><strong>TIMEZONE</strong> – {t('subscriber\'s timezone (eg. "Europe/Tallinn", "PST" or "UTC"). If not set defaults to "UTC"')}</li>
|
||||
<li><strong>MERGE_TAG_VALUE</strong> – {t('custom field value. Use yes/no for option group values (checkboxes, radios, drop downs)')}</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
{t('Additional POST arguments')}:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>FORCE_SUBSCRIBE</strong> – {t('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')}
|
||||
by default.
|
||||
</li>
|
||||
<li>
|
||||
<strong>REQUIRE_CONFIRMATION</strong> – {t('set to "yes" if you want to send confirmation email to the subscriber before actually marking as subscribed')}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>{t('Example')}</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 – {t('Remove subscription')}</h3>
|
||||
|
||||
<p>
|
||||
{t('This API call marks a subscription as unsubscribed')}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>GET</strong> {t('arguments')}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>access_token</strong> – {t('your personal access token')}</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>POST</strong> {t('arguments')}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>EMAIL</strong> – {t('subscriber\'s email address')} (<em>{t('required')}</em>)</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST {serviceUrl}api/unsubscribe/B16uVTdW?access_token={accessToken} \
|
||||
--data 'EMAIL=test@example.com'</pre>
|
||||
|
||||
<h3>POST /api/delete/:listId – {t('Delete subscription')}</h3>
|
||||
|
||||
<p>
|
||||
{t('This API call deletes a subscription')}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>GET</strong> {t('arguments')}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>access_token</strong> – {t('your personal access token')}</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>POST</strong> {t('arguments')}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>EMAIL</strong> – {t('subscriber\'s email address')} (<em>{t('required')}</em>)</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST {serviceUrl}api/delete/B16uVTdW?access_token={accessToken} \
|
||||
--data 'EMAIL=test@example.com'</pre>
|
||||
|
||||
<h3>GET /api/blacklist/get – {t('Get list of blacklisted emails')}</h3>
|
||||
|
||||
<p>
|
||||
{t('This API call get list of blacklisted emails.')}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>GET</strong> {t('arguments')}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>access_token</strong> – {t('your personal access token')}
|
||||
<li><strong>start</strong> – {t('Start position')} (<em>{t('optional, default 0')}</em>)</li>
|
||||
<li><strong>limit</strong> – {t('limit emails count in response')} (<em>{t('optional, default 10000')}</em>)</li>
|
||||
<li><strong>search</strong> – {t('filter by part of email')} (<em>{t('optional, default ""')}</em>)</li>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XGET '{serviceUrl}api/blacklist/get?access_token={accessToken}&limit=10&start=10&search=gmail' </pre>
|
||||
|
||||
<h3>POST /api/blacklist/add – {t('Add email to blacklist')}</h3>
|
||||
|
||||
<p>
|
||||
{t('This API call either add emails to blacklist')}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>GET</strong> {t('arguments')}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>access_token</strong> – {t('your personal access token')}</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>POST</strong> {t('arguments')}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>EMAIL</strong> – {t('email address')} (<em>{t('required')}</em>)</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST '{serviceUrl}api/blacklist/add?access_token={accessToken}' \
|
||||
--data 'EMAIL=test@example.com&'</pre>
|
||||
|
||||
<h3>POST /api/blacklist/delete – {t('Delete email from blacklist')}</h3>
|
||||
|
||||
<p>
|
||||
{t('This API call either delete emails from blacklist')}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>GET</strong> {t('arguments')}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>access_token</strong> – {t('your personal access token')}</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>POST</strong> {t('arguments')}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>EMAIL</strong> – {t('email address')} (<em>{t('required')}</em>)</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST '{serviceUrl}api/blacklist/delete?access_token={accessToken}' \
|
||||
--data 'EMAIL=test@example.com&'</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from '../lib/form';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import passwordValidator from '../../../shared/password-validator';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -23,15 +24,15 @@ export default class Account extends Component {
|
|||
|
||||
this.initForm({
|
||||
serverValidation: {
|
||||
url: '/account/rest/account-validate',
|
||||
changed: ['email', 'password', 'currentPassword']
|
||||
url: '/rest/account-validate',
|
||||
changed: ['email', 'username', 'currentPassword']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(`/account/rest/account`, data => {
|
||||
await this.getFormValuesFromURL('/rest/account', data => {
|
||||
data.password = '';
|
||||
data.password2 = '';
|
||||
data.currentPassword = '';
|
||||
|
@ -102,15 +103,14 @@ export default class Account extends Component {
|
|||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
|
||||
try {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Updating user profile ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/account/rest/account', data => {
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/account', data => {
|
||||
delete data.password2;
|
||||
});
|
||||
|
||||
this.enableForm();
|
||||
|
||||
if (submitSuccessful) {
|
||||
this.setFlashMessage('success', t('User profile updated'));
|
||||
this.hideFormValidation();
|
||||
|
@ -122,11 +122,43 @@ export default class Account extends Component {
|
|||
} else {
|
||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof interoperableErrors.IncorrectPasswordError) {
|
||||
this.enableForm();
|
||||
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('The password is incorrect (possibly just changed in another window / session). Enter correct password and try again.')}
|
||||
</span>
|
||||
);
|
||||
|
||||
this.scheduleFormRevalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof interoperableErrors.DuplicitEmailError) {
|
||||
this.enableForm();
|
||||
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('The email is already assigned to another user. Enter another email and try again.')}
|
||||
</span>
|
||||
);
|
||||
|
||||
this.scheduleFormRevalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
if (mailtrainConfig.isAuthMethodLocal) {
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Account')}</Title>
|
||||
|
@ -150,5 +182,12 @@ export default class Account extends Component {
|
|||
</Form>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
<div>
|
||||
<Title>{t('Account')}</Title>
|
||||
|
||||
<p>Account management is not possible because Mailtrain is configured to use externally managed users.</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
78
client/src/account/Forgot.js
Normal file
78
client/src/account/Forgot.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { withPageHelpers, Title } from '../lib/page'
|
||||
import {
|
||||
withForm, Form, FormSendMethod, InputField, CheckBox, ButtonRow, Button, AlignedRow
|
||||
} from '../lib/form';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
export default class Forget extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.populateFormValues({
|
||||
usernameOrEmail: this.props.match.params.username || ''
|
||||
});
|
||||
}
|
||||
|
||||
localValidateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
|
||||
const username = state.getIn(['usernameOrEmail', 'value']);
|
||||
if (!username) {
|
||||
state.setIn(['usernameOrEmail', 'error'], t('Username or email must not be empty'));
|
||||
} else {
|
||||
state.setIn(['usernameOrEmail', 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Processing ...'));
|
||||
|
||||
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.'));
|
||||
} else {
|
||||
this.enableForm();
|
||||
this.setFormStatusMessage('warning', t('Please enter your username / email and try again.'));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Password Reset')}</Title>
|
||||
|
||||
<p>{t('Please provide the username or email address that is registered with your Mailtrain account.')}</p>
|
||||
|
||||
<p>{t('We will send you an email that will allow you to reset your password.')}</p>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="usernameOrEmail" label={t('Username or email')}/>
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Send email')}/>
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
107
client/src/account/Login.js
Normal file
107
client/src/account/Login.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
'use strict';
|
||||
|
||||
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, FormSendMethod, InputField, CheckBox, ButtonRow, Button, AlignedRow
|
||||
} from '../lib/form';
|
||||
import { withErrorHandling } from '../lib/error-handling';
|
||||
import URL from 'url-parse';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
export default class Login extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.populateFormValues({
|
||||
username: '',
|
||||
password: '',
|
||||
remember: false
|
||||
});
|
||||
}
|
||||
|
||||
localValidateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
|
||||
const username = state.getIn(['username', 'value']);
|
||||
if (!username) {
|
||||
state.setIn(['username', 'error'], t('User name must not be empty'));
|
||||
} else {
|
||||
state.setIn(['username', 'error'], null);
|
||||
}
|
||||
|
||||
const password = state.getIn(['password', 'value']);
|
||||
if (!username) {
|
||||
state.setIn(['password', 'error'], t('Password must not be empty'));
|
||||
} else {
|
||||
state.setIn(['password', 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
|
||||
try {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Verifying credentials ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/login');
|
||||
|
||||
if (submitSuccessful) {
|
||||
const query = new URL(this.props.location.search, true).query;
|
||||
|
||||
/* FIXME, once we turn Mailtrain to single-page application, this should become navigateTo */
|
||||
window.location = query.next;
|
||||
} else {
|
||||
this.setFormStatusMessage('warning', t('Please enter your credentials and try again.'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof interoperableErrors.IncorrectPasswordError) {
|
||||
this.enableForm();
|
||||
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Invalid username or password.')}</strong>
|
||||
</span>
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Sign in')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="username" label={t('Username')}/>
|
||||
<InputField id="password" label={t('Password')} type="password" />
|
||||
<CheckBox id="remember" label={t('Remember me')}/>
|
||||
|
||||
<ButtonRow>
|
||||
<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>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
137
client/src/account/Reset.js
Normal file
137
client/src/account/Reset.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import { withPageHelpers, Title } from '../lib/page'
|
||||
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';
|
||||
|
||||
const ResetTokenValidationState = {
|
||||
PENDING: 0,
|
||||
VALID: 1,
|
||||
INVALID: 2
|
||||
};
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
export default class Account extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.passwordValidator = passwordValidator(props.t);
|
||||
|
||||
this.state = {
|
||||
resetTokenValidationState: ResetTokenValidationState.PENDING
|
||||
};
|
||||
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadAccessToken() {
|
||||
const params = this.props.match.params;
|
||||
|
||||
const response = await axios.post('/rest/password-reset-validate', {
|
||||
username: params.username,
|
||||
resetToken: params.resetToken
|
||||
});
|
||||
|
||||
this.setState('resetTokenValidationState', response.data ? ResetTokenValidationState.VALID : ResetTokenValidationState.INVALID);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const params = this.props.match.params;
|
||||
|
||||
this.populateFormValues({
|
||||
username: params.username,
|
||||
resetToken: params.resetToken,
|
||||
password: '',
|
||||
password2: ''
|
||||
});
|
||||
}
|
||||
|
||||
localValidateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
|
||||
const password = state.getIn(['password', 'value']) || '';
|
||||
const password2 = state.getIn(['password2', 'value']) || '';
|
||||
|
||||
let passwordMsgs = [];
|
||||
|
||||
if (password || currentPassword) {
|
||||
const passwordResults = this.passwordValidator.test(password);
|
||||
passwordMsgs.push(...passwordResults.errors);
|
||||
}
|
||||
|
||||
if (passwordMsgs.length > 1) {
|
||||
passwordMsgs = passwordMsgs.map((msg, idx) => <div key={idx}>{msg}</div>)
|
||||
}
|
||||
|
||||
state.setIn(['password', 'error'], passwordMsgs.length > 0 ? passwordMsgs : null);
|
||||
state.setIn(['password2', 'error'], password !== password2 ? t('Passwords must match') : null);
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
|
||||
try {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Resetting password ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/password-reset', data => {
|
||||
delete data.password2;
|
||||
});
|
||||
|
||||
if (submitSuccessful) {
|
||||
this.navigateToWithFlashMessage('/account/login', 'success', t('Password reset'));
|
||||
} else {
|
||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof interoperableErrors.InvalidToken) {
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your password cannot be reset.')}</strong>{' '}
|
||||
{t('The reset token has expired.')}{' '}<Link to={`/account/forgot/${this.getFormValue('username')}`}>{t('Click here to request a new password reset link.')}</Link>
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
if (this.state.resetTokenValidationState === ResetTokenValidationState.PENDING) {
|
||||
return (
|
||||
<div>{t('Validating password reset token ...')}</div>
|
||||
)
|
||||
} else if (this.state.resetTokenValidationState === ResetTokenValidationState.INVALID) {
|
||||
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Set new password for') + ' ' + this.getFormValue('username')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="password" label={t('New Password')} type="password"/>
|
||||
<InputField id="password2" label={t('Confirm Password')} type="password"/>
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Reset password')}/>
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,20 +7,60 @@ import i18n from '../lib/i18n';
|
|||
|
||||
import { Section } from '../lib/page'
|
||||
import Account from './Account'
|
||||
import Login from './Login'
|
||||
import Reset from './Forgot'
|
||||
import ResetLink from './Reset'
|
||||
import API from './API'
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
const getStructure = t => ({
|
||||
|
||||
const getStructure = t => {
|
||||
const subPaths = {
|
||||
'login': {
|
||||
title: t('Sign in'),
|
||||
link: '/account/login',
|
||||
component: Login,
|
||||
},
|
||||
'api': {
|
||||
title: t('API'),
|
||||
link: '/account/api',
|
||||
component: API
|
||||
}
|
||||
};
|
||||
|
||||
if (mailtrainConfig.isAuthMethodLocal) {
|
||||
subPaths.forgot = {
|
||||
title: t('Password reset'),
|
||||
params: [':username?'],
|
||||
link: '/account/forgot',
|
||||
component: Reset
|
||||
};
|
||||
|
||||
subPaths.reset = {
|
||||
title: t('Password reset'),
|
||||
params: [':username', ':resetToken'],
|
||||
link: '/account/reset',
|
||||
component: ResetLink
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
'': {
|
||||
title: t('Home'),
|
||||
externalLink: '/',
|
||||
children: {
|
||||
'account': {
|
||||
account: {
|
||||
title: t('Account'),
|
||||
link: '/account',
|
||||
component: Account
|
||||
component: Account,
|
||||
|
||||
children: subPaths
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default function() {
|
||||
ReactDOM.render(
|
||||
|
|
27
client/src/lib/bootstrap-components.js
vendored
27
client/src/lib/bootstrap-components.js
vendored
|
@ -74,6 +74,32 @@ class Button extends Component {
|
|||
}
|
||||
|
||||
|
||||
@withErrorHandling
|
||||
class ActionLink extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClick(evt) {
|
||||
if (this.props.onClickAsync) {
|
||||
evt.preventDefault();
|
||||
|
||||
await this.props.onClickAsync(evt);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
return (
|
||||
<a href="" className={props.className} onClick={::this.onClick}>{props.children}</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@translate()
|
||||
@withErrorHandling
|
||||
class ModalDialog extends Component {
|
||||
|
@ -184,6 +210,7 @@ class ModalDialog extends Component {
|
|||
|
||||
export {
|
||||
Button,
|
||||
ActionLink,
|
||||
DismissibleAlert,
|
||||
ModalDialog
|
||||
};
|
|
@ -128,6 +128,20 @@ function wrapInput(id, htmlId, owner, label, help, input) {
|
|||
);
|
||||
}
|
||||
|
||||
function wrapInputInline(id, htmlId, owner, containerClass, label, help, input) {
|
||||
const helpBlock = help ? <div className="help-block col-sm-offset-2 col-sm-10" id={htmlId + '_help'}>{help}</div> : '';
|
||||
|
||||
return (
|
||||
<div className={owner.addFormValidationClass('form-group', id)} >
|
||||
<div className={"col-sm-10 col-sm-offset-2 " + containerClass }>
|
||||
<label>{input} {label}</label>
|
||||
</div>
|
||||
{helpBlock}
|
||||
<div className="help-block col-sm-offset-2 col-sm-10" id={htmlId + '_help_validation'}>{owner.getFormValidationMessage(id)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
class InputField extends Component {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
|
@ -162,6 +176,29 @@ class InputField extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
class CheckBox extends Component {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
help: PropTypes.string
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
formStateOwner: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const owner = this.context.formStateOwner;
|
||||
const id = this.props.id;
|
||||
const htmlId = 'form_' + id;
|
||||
|
||||
return wrapInputInline(id, htmlId, owner, 'checkbox', props.label, props.help,
|
||||
<input type="checkbox" checked={owner.getFormValue(id)} id={htmlId} aria-describedby={htmlId + '_help'} onChange={evt => {console.log(evt); /* FIXME owner.updateFormValue(id, evt.target.value)*/ }}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextArea extends Component {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
|
@ -186,11 +223,19 @@ class TextArea extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
class ButtonRow extends Component {
|
||||
class AlignedRow extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
className: ''
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-10 col-sm-offset-2 mt-button-row">
|
||||
<div className={"col-sm-10 col-sm-offset-2 " + this.props.className}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -198,11 +243,19 @@ class ButtonRow extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class ButtonRow extends Component {
|
||||
render() {
|
||||
return (
|
||||
<AlignedRow className="mt-button-row">{this.props.children}</AlignedRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@withErrorHandling
|
||||
class Button extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
label: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
@ -215,12 +268,7 @@ class Button extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async onClick(evt) {
|
||||
if (this.props.onClick) {
|
||||
evt.preventDefault();
|
||||
|
||||
onClick(evt);
|
||||
|
||||
} else if (this.props.onClickAsync) {
|
||||
if (this.props.onClickAsync) {
|
||||
evt.preventDefault();
|
||||
|
||||
this.context.formStateOwner.disableForm();
|
||||
|
@ -255,6 +303,7 @@ class Button extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class TreeTableSelect extends Component {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
|
@ -297,6 +346,93 @@ function withForm(target) {
|
|||
isServerValidationRunning: false
|
||||
});
|
||||
|
||||
// formValidateResolve is called by "validateForm" once client receives validation response from server that does not
|
||||
// trigger another server validation
|
||||
let formValidateResolve = null;
|
||||
|
||||
function scheduleValidateForm(self) {
|
||||
setTimeout(() => {
|
||||
self.setState(previousState => ({
|
||||
formState: previousState.formState.withMutations(mutState => {
|
||||
validateFormState(self, mutState);
|
||||
})
|
||||
}));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function validateFormState(self, mutState) {
|
||||
const settings = self.state.formSettings;
|
||||
|
||||
if (!mutState.get('isServerValidationRunning') && settings.serverValidation) {
|
||||
const payload = {};
|
||||
let payloadNotEmpty = false;
|
||||
|
||||
for (const attr of settings.serverValidation.extra || []) {
|
||||
payload[attr] = mutState.getIn(['data', attr, 'value']);
|
||||
}
|
||||
|
||||
for (const attr of settings.serverValidation.changed) {
|
||||
const currValue = mutState.getIn(['data', attr, 'value']);
|
||||
const serverValue = mutState.getIn(['data', attr, 'serverValue']);
|
||||
|
||||
// This really assumes that all form values are preinitialized (i.e. not undef)
|
||||
if (currValue !== serverValue) {
|
||||
mutState.setIn(['data', attr, 'serverValidated'], false);
|
||||
payload[attr] = currValue;
|
||||
payloadNotEmpty = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (payloadNotEmpty) {
|
||||
mutState.set('isServerValidationRunning', true);
|
||||
|
||||
axios.post(settings.serverValidation.url, payload)
|
||||
.then(response => {
|
||||
|
||||
self.setState(previousState => ({
|
||||
formState: previousState.formState.withMutations(mutState => {
|
||||
mutState.set('isServerValidationRunning', false);
|
||||
|
||||
mutState.update('data', stateData => stateData.withMutations(mutStateData => {
|
||||
for (const attr in payload) {
|
||||
mutStateData.setIn([attr, 'serverValue'], payload[attr]);
|
||||
|
||||
if (payload[attr] === mutState.getIn(['data', attr, 'value'])) {
|
||||
mutStateData.setIn([attr, 'serverValidated'], true);
|
||||
mutStateData.setIn([attr, 'serverValidation'], response.data[attr] || true);
|
||||
}
|
||||
}
|
||||
}));
|
||||
})
|
||||
}));
|
||||
|
||||
scheduleValidateForm(self);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Ignoring unhandled error in "validateFormState": ' + error);
|
||||
|
||||
self.setState(previousState => ({
|
||||
formState: previousState.formState.set('isServerValidationRunning', false)
|
||||
}));
|
||||
|
||||
scheduleValidateForm(self);
|
||||
});
|
||||
} else {
|
||||
if (formValidateResolve) {
|
||||
const resolve = formValidateResolve;
|
||||
formValidateResolve = null;
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self.localValidateFormValues) {
|
||||
mutState.update('data', stateData => stateData.withMutations(mutStateData => {
|
||||
self.localValidateFormValues(mutStateData);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
inst.initForm = function(settings) {
|
||||
const state = this.state || {};
|
||||
state.formState = cleanFormState;
|
||||
|
@ -345,12 +481,15 @@ function withForm(target) {
|
|||
mutator(data);
|
||||
}
|
||||
|
||||
let response;
|
||||
if (method === FormSendMethod.PUT) {
|
||||
await axios.put(url, data);
|
||||
response = await axios.put(url, data);
|
||||
} else if (method === FormSendMethod.POST) {
|
||||
await axios.post(url, data);
|
||||
response = await axios.post(url, data);
|
||||
}
|
||||
return true;
|
||||
|
||||
return response.data || true;
|
||||
|
||||
} else {
|
||||
this.showFormValidation();
|
||||
return false;
|
||||
|
@ -371,110 +510,26 @@ function withForm(target) {
|
|||
}
|
||||
}));
|
||||
|
||||
this.validateForm(mutState);
|
||||
validateFormState(this, mutState);
|
||||
})
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
// formValidateResolve is called by "validateForm" once client receives validation response from server that does not
|
||||
// trigger another server validation
|
||||
let formValidateResolve = null;
|
||||
|
||||
const scheduleValidateForm = (self) => {
|
||||
setTimeout(() => {
|
||||
self.setState(previousState => ({
|
||||
formState: previousState.formState.withMutations(mutState => {
|
||||
self.validateForm(mutState);
|
||||
})
|
||||
}));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
inst.waitForFormServerValidated = async function() {
|
||||
if (!this.isFormServerValidated()) {
|
||||
await new Promise(resolve => { formValidateResolve = resolve; });
|
||||
}
|
||||
};
|
||||
|
||||
inst.validateForm = function(mutState) {
|
||||
const settings = this.state.formSettings;
|
||||
|
||||
if (!mutState.get('isServerValidationRunning') && settings.serverValidation) {
|
||||
const payload = {};
|
||||
let payloadNotEmpty = false;
|
||||
|
||||
for (const attr of settings.serverValidation.extra || []) {
|
||||
payload[attr] = mutState.getIn(['data', attr, 'value']);
|
||||
}
|
||||
|
||||
for (const attr of settings.serverValidation.changed) {
|
||||
const currValue = mutState.getIn(['data', attr, 'value']);
|
||||
const serverValue = mutState.getIn(['data', attr, 'serverValue']);
|
||||
|
||||
// This really assumes that all form values are preinitialized (i.e. not undef)
|
||||
if (currValue !== serverValue) {
|
||||
mutState.setIn(['data', attr, 'serverValidated'], false);
|
||||
payload[attr] = currValue;
|
||||
payloadNotEmpty = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (payloadNotEmpty) {
|
||||
mutState.set('isServerValidationRunning', true);
|
||||
|
||||
axios.post(settings.serverValidation.url, payload)
|
||||
.then(response => {
|
||||
|
||||
this.setState(previousState => ({
|
||||
formState: previousState.formState.withMutations(mutState => {
|
||||
mutState.set('isServerValidationRunning', false);
|
||||
|
||||
mutState.update('data', stateData => stateData.withMutations(mutStateData => {
|
||||
for (const attr in payload) {
|
||||
mutStateData.setIn([attr, 'serverValue'], payload[attr]);
|
||||
|
||||
if (payload[attr] === mutState.getIn(['data', attr, 'value'])) {
|
||||
mutStateData.setIn([attr, 'serverValidated'], true);
|
||||
mutStateData.setIn([attr, 'serverValidation'], response.data[attr] || true);
|
||||
}
|
||||
}
|
||||
}));
|
||||
})
|
||||
}));
|
||||
|
||||
inst.scheduleFormRevalidate = function() {
|
||||
scheduleValidateForm(this);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Ignoring unhandled error in "validateForm": ' + error);
|
||||
|
||||
this.setState(previousState => ({
|
||||
formState: previousState.formState.set('isServerValidationRunning', false)
|
||||
}));
|
||||
|
||||
scheduleValidateForm(this);
|
||||
});
|
||||
} else {
|
||||
if (formValidateResolve) {
|
||||
const resolve = formValidateResolve;
|
||||
formValidateResolve = null;
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.localValidateFormValues) {
|
||||
mutState.update('data', stateData => stateData.withMutations(mutStateData => {
|
||||
this.localValidateFormValues(mutStateData);
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
inst.updateFormValue = function(key, value) {
|
||||
this.setState(previousState => ({
|
||||
formState: previousState.formState.withMutations(mutState => {
|
||||
mutState.setIn(['data', key, 'value'], value);
|
||||
this.validateForm(mutState);
|
||||
validateFormState(this, mutState);
|
||||
})
|
||||
}));
|
||||
};
|
||||
|
@ -590,7 +645,9 @@ export {
|
|||
Form,
|
||||
Fieldset,
|
||||
InputField,
|
||||
CheckBox,
|
||||
TextArea,
|
||||
AlignedRow,
|
||||
ButtonRow,
|
||||
Button,
|
||||
TreeTableSelect,
|
||||
|
|
|
@ -202,7 +202,8 @@ class SectionContent extends Component {
|
|||
|
||||
errorHandler(error) {
|
||||
if (error instanceof interoperableErrors.NotLoggedInError) {
|
||||
window.location = '/users/login?next=' + encodeURIComponent(this.props.root);
|
||||
/* FIXME, once we turn Mailtrain to single-page application, this should become navigateTo */
|
||||
window.location = '/account/login?next=' + encodeURIComponent(this.props.root);
|
||||
} else if (error.response && error.response.data && error.response.data.message) {
|
||||
this.navigateToWithFlashMessage(this.props.root, 'danger', error.response.data.message);
|
||||
} else {
|
||||
|
@ -294,7 +295,6 @@ class NavButton extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function withPageHelpers(target) {
|
||||
withErrorHandling(target);
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ export default class CUD extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async loadTreeData() {
|
||||
axios.get('/namespaces/rest/namespaces-tree')
|
||||
axios.get('/rest/namespaces-tree')
|
||||
.then(response => {
|
||||
|
||||
response.data.expanded = true;
|
||||
|
@ -75,7 +75,7 @@ export default class CUD extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(`/namespaces/rest/namespaces/${this.state.entityId}`, data => {
|
||||
await this.getFormValuesFromURL(`/rest/namespaces/${this.state.entityId}`, data => {
|
||||
if (data.parent) data.parent = data.parent.toString();
|
||||
});
|
||||
}
|
||||
|
@ -121,10 +121,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (edit) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/namespaces/rest/namespaces/${this.state.entityId}`
|
||||
url = `/rest/namespaces/${this.state.entityId}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/namespaces/rest/namespaces'
|
||||
url = '/rest/namespaces'
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -175,7 +175,7 @@ export default class CUD extends Component {
|
|||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Deleting namespace...'));
|
||||
|
||||
await axios.delete(`/namespaces/rest/namespaces/${this.state.entityId}`);
|
||||
await axios.delete(`/rest/namespaces/${this.state.entityId}`);
|
||||
|
||||
this.navigateToWithFlashMessage('/namespaces', 'success', t('Namespace deleted'));
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Namespaces')}</Title>
|
||||
|
||||
<TreeTable withHeader dataUrl="/namespaces/rest/namespaces-tree" actionLinks={actionLinks} />
|
||||
<TreeTable withHeader dataUrl="/rest/namespaces-tree" actionLinks={actionLinks} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,17 +14,17 @@ const getStructure = t => ({
|
|||
title: t('Home'),
|
||||
externalLink: '/',
|
||||
children: {
|
||||
'namespaces': {
|
||||
namespaces: {
|
||||
title: t('Namespaces'),
|
||||
link: '/namespaces',
|
||||
component: List,
|
||||
children: {
|
||||
'edit' : {
|
||||
edit : {
|
||||
title: t('Edit Namespace'),
|
||||
params: [':id', ':action?'],
|
||||
render: props => (<CUD edit {...props} />)
|
||||
},
|
||||
'create' : {
|
||||
create : {
|
||||
title: t('Create Namespace'),
|
||||
render: props => (<CUD {...props} />)
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export default class CUD extends Component {
|
|||
|
||||
this.initForm({
|
||||
serverValidation: {
|
||||
url: '/users/rest/users-validate',
|
||||
url: '/rest/users-validate',
|
||||
changed: ['username', 'email'],
|
||||
extra: ['id']
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ export default class CUD extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(`/users/rest/users/${this.state.entityId}`, data => {
|
||||
await this.getFormValuesFromURL(`/rest/users/${this.state.entityId}`, data => {
|
||||
data.password = '';
|
||||
data.password2 = '';
|
||||
});
|
||||
|
@ -66,7 +66,6 @@ export default class CUD extends Component {
|
|||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
|
||||
const username = state.getIn(['username', 'value']);
|
||||
const usernameServerValidation = state.getIn(['username', 'serverValidation']);
|
||||
|
||||
|
@ -88,8 +87,6 @@ export default class CUD extends Component {
|
|||
state.setIn(['email', 'error'], t('Email must not be empty'));
|
||||
} else if (!emailServerValidation || emailServerValidation.invalid) {
|
||||
state.setIn(['email', 'error'], t('Invalid email address.'));
|
||||
} else if (!emailServerValidation || emailServerValidation.exists) {
|
||||
state.setIn(['email', 'error'], t('The email is already associated with another user in the system.'));
|
||||
} else {
|
||||
state.setIn(['email', 'error'], null);
|
||||
}
|
||||
|
@ -107,6 +104,8 @@ export default class CUD extends Component {
|
|||
const password = state.getIn(['password', 'value']) || '';
|
||||
const password2 = state.getIn(['password2', 'value']) || '';
|
||||
|
||||
const passwordResults = this.passwordValidator.test(password);
|
||||
|
||||
let passwordMsgs = [];
|
||||
|
||||
if (!edit && !password) {
|
||||
|
@ -114,7 +113,6 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
if (password) {
|
||||
const passwordResults = this.passwordValidator.test(password);
|
||||
passwordMsgs.push(...passwordResults.errors);
|
||||
}
|
||||
|
||||
|
@ -133,10 +131,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (edit) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/users/rest/users/${this.state.entityId}`
|
||||
url = `/rest/users/${this.state.entityId}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/users/rest/users'
|
||||
url = '/rest/users'
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -194,7 +192,7 @@ export default class CUD extends Component {
|
|||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Deleting user...'));
|
||||
|
||||
await axios.delete(`/users/rest/users/${this.state.entityId}`);
|
||||
await axios.delete(`/rest/users/${this.state.entityId}`);
|
||||
|
||||
this.navigateToWithFlashMessage('/users', 'success', t('User deleted'));
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import React, { Component } from 'react';
|
|||
import { translate } from 'react-i18next';
|
||||
import { Title, Toolbar, NavButton } from '../lib/page';
|
||||
import { Table } from '../lib/table';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
@translate()
|
||||
export default class List extends Component {
|
||||
|
@ -19,10 +20,13 @@ export default class List extends Component {
|
|||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
{ data: 1, title: "Username" },
|
||||
{ data: 2, title: "Full Name" }
|
||||
{ data: 1, title: "Username" }
|
||||
];
|
||||
|
||||
if (mailtrainConfig.isAuthMethodLocal) {
|
||||
columns.push({ data: 2, title: "Full Name" });
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Toolbar>
|
||||
|
@ -31,7 +35,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Users')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/users/rest/users-table" columns={columns} actionLinks={actionLinks} />
|
||||
<Table withHeader dataUrl="/rest/users-table" columns={columns} actionLinks={actionLinks} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,31 +8,39 @@ import i18n from '../lib/i18n';
|
|||
import { Section } from '../lib/page'
|
||||
import CUD from './CUD'
|
||||
import List from './List'
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
const getStructure = t => ({
|
||||
const getStructure = t => {
|
||||
const subPaths = {};
|
||||
|
||||
if (mailtrainConfig.isAuthMethodLocal) {
|
||||
subPaths.edit = {
|
||||
title: t('Edit User'),
|
||||
params: [':id', ':action?'],
|
||||
render: props => (<CUD edit {...props} />)
|
||||
};
|
||||
|
||||
subPahts.create = {
|
||||
title: t('Create User'),
|
||||
render: props => (<CUD {...props} />)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
'': {
|
||||
title: t('Home'),
|
||||
externalLink: '/',
|
||||
children: {
|
||||
'users': {
|
||||
users: {
|
||||
title: t('Users'),
|
||||
link: '/users',
|
||||
component: List,
|
||||
children: {
|
||||
'edit' : {
|
||||
title: t('Edit User'),
|
||||
params: [':id', ':action?'],
|
||||
render: props => (<CUD edit {...props} />)
|
||||
},
|
||||
'create' : {
|
||||
title: t('Create User'),
|
||||
render: props => (<CUD {...props} />)
|
||||
children: subPaths
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default function() {
|
||||
ReactDOM.render(
|
||||
|
|
|
@ -21,7 +21,8 @@ module.exports = {
|
|||
},
|
||||
externals: {
|
||||
jquery: 'jQuery',
|
||||
csfrToken: 'csfrToken'
|
||||
csfrToken: 'csfrToken',
|
||||
mailtrainConfig: 'mailtrainConfig'
|
||||
},
|
||||
plugins: [
|
||||
// new webpack.optimize.UglifyJsPlugin(),
|
||||
|
|
|
@ -119,6 +119,8 @@ baseDN="ou=users,dc=company"
|
|||
filter="(|(username={{username}})(mail={{username}}))"
|
||||
#Username field in LDAP (uid/cn/username)
|
||||
uidTag="username"
|
||||
# nameTag identifies the attribute to be used for user's full name
|
||||
nameTag="username"
|
||||
passwordresetlink=""
|
||||
|
||||
[postfixbounce]
|
||||
|
|
26
lib/client-helpers.js
Normal file
26
lib/client-helpers.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('./passport');
|
||||
|
||||
function _getConfig() {
|
||||
return {
|
||||
authMethod: passport.authMethod,
|
||||
isAuthMethodLocal: passport.isAuthMethodLocal
|
||||
}
|
||||
}
|
||||
|
||||
function registerRootRoute(router, title, entryPoint) {
|
||||
router.get('/*', passport.csrfProtection, (req, res) => {
|
||||
res.render('react-root', {
|
||||
title,
|
||||
reactEntryPoint: entryPoint,
|
||||
reactCsrfToken: req.csrfToken(),
|
||||
mailtrainConfig: JSON.stringify(_getConfig())
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
registerRootRoute
|
||||
};
|
||||
|
|
@ -39,52 +39,6 @@ module.exports.get = (id, callback) => {
|
|||
});
|
||||
};
|
||||
|
||||
module.exports.findByAccessToken = (accessToken, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT `id`, `username`, `email`, `access_token` FROM `users` WHERE `access_token`=? LIMIT 1', [accessToken], (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.findByUsername = (username, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT `id`, `username`, `email`, `access_token` FROM `users` WHERE `username`=? LIMIT 1', [username], (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) {
|
15
lib/nodeify.js
Normal file
15
lib/nodeify.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
const nodeify = require('nodeify');
|
||||
|
||||
module.exports.nodeifyPromise = nodeify;
|
||||
|
||||
module.exports.nodeifyFunction = (asyncFun) => {
|
||||
return (...args) => {
|
||||
const callback = args.pop();
|
||||
|
||||
const promise = asyncFun(...args);
|
||||
|
||||
return module.exports.nodeifyPromise(promise, callback);
|
||||
};
|
||||
};
|
117
lib/passport.js
117
lib/passport.js
|
@ -10,7 +10,10 @@ let LocalStrategy = require('passport-local').Strategy;
|
|||
|
||||
let csrf = require('csurf');
|
||||
let bodyParser = require('body-parser');
|
||||
let users = require('./models/users-legacy');
|
||||
|
||||
const users = require('../models/users');
|
||||
const { nodeifyFunction, nodeifyPromise } = require('./nodeify');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
|
||||
let LdapStrategy;
|
||||
try {
|
||||
|
@ -30,28 +33,30 @@ module.exports.parseForm = bodyParser.urlencoded({
|
|||
limit: config.www.postsize
|
||||
});
|
||||
|
||||
module.exports.loggedIn = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
next(new interoperableErrors.NotLoggedInError());
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.setup = app => {
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
};
|
||||
|
||||
module.exports.logout = (req, res) => {
|
||||
if (req.user) {
|
||||
req.flash('info', util.format(_('%s logged out'), req.user.username));
|
||||
module.exports.restLogout = (req, res) => {
|
||||
req.logout();
|
||||
}
|
||||
res.redirect('/');
|
||||
res.json();
|
||||
};
|
||||
|
||||
module.exports.login = (req, res, next) => {
|
||||
module.exports.restLogin = (req, res, next) => {
|
||||
passport.authenticate(config.ldap.enabled ? 'ldap' : 'local', (err, user, info) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message);
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
req.flash('danger', info && info.message || _('Failed to authenticate user'));
|
||||
return res.redirect('/users/login' + (req.body.next ? '?next=' + encodeURIComponent(req.body.next) : ''));
|
||||
return next(new interoperableErrors.IncorrectPasswordError());
|
||||
}
|
||||
req.logIn(user, err => {
|
||||
if (err) {
|
||||
|
@ -66,14 +71,15 @@ module.exports.login = (req, res, next) => {
|
|||
req.session.cookie.expires = false;
|
||||
}
|
||||
|
||||
req.flash('success', util.format(_('Logged in as %s'), user.username));
|
||||
return res.redirect(req.body.next || '/');
|
||||
return res.json();
|
||||
});
|
||||
})(req, res, next);
|
||||
};
|
||||
|
||||
if (config.ldap.enabled && LdapStrategy) {
|
||||
log.info('Using LDAP auth');
|
||||
module.exports.authMethod = 'ldap';
|
||||
module.exports.isAuthMethodLocal = false;
|
||||
|
||||
let opts = {
|
||||
server: {
|
||||
|
@ -82,62 +88,55 @@ if (config.ldap.enabled && LdapStrategy) {
|
|||
base: config.ldap.baseDN,
|
||||
search: {
|
||||
filter: config.ldap.filter,
|
||||
attributes: [config.ldap.uidTag, 'mail'],
|
||||
attributes: [config.ldap.uidTag, config.ldap.nameTag, 'mail'],
|
||||
scope: 'sub'
|
||||
},
|
||||
uidTag: config.ldap.uidTag
|
||||
};
|
||||
|
||||
passport.use(new LdapStrategy(opts, (profile, done) => {
|
||||
users.findByUsername(profile[config.ldap.uidTag], (err, user) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
passport.use(new LdapStrategy(opts, nodeifyFunction(async (profile) => {
|
||||
try {
|
||||
const user = await users.getByUsername(profile[config.ldap.uidTag]);
|
||||
|
||||
if (!user) {
|
||||
// password is empty for ldap
|
||||
users.add(profile[config.ldap.uidTag], '', profile.mail, (err, id) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
return done(null, {
|
||||
id,
|
||||
username: profile[config.ldap.uidTag]
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return done(null, {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username
|
||||
username: user.username,
|
||||
name: profile[config.ldap.nameTag],
|
||||
email: profile.mail
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
if (err instanceof interoperableErrors.NotFoundError) {
|
||||
const userId = await users.createExternal({
|
||||
username: profile[config.ldap.uidTag],
|
||||
});
|
||||
|
||||
return {
|
||||
id: userId,
|
||||
username: profile[config.ldap.uidTag],
|
||||
name: profile[config.ldap.nameTag],
|
||||
email: profile.mail
|
||||
};
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
}
|
||||
})));
|
||||
|
||||
passport.serializeUser((user, done) => { /* FIXME */ console.log(user); done(null, user); });
|
||||
passport.deserializeUser((user, done) => done(null, user));
|
||||
|
||||
} else {
|
||||
log.info('Using local auth');
|
||||
module.exports.authMethod = 'local';
|
||||
module.exports.isAuthMethodLocal = true;
|
||||
|
||||
passport.use(new LocalStrategy((username, password, done) => {
|
||||
users.authenticate(username, password, (err, user) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
passport.use(new LocalStrategy(nodeifyFunction(async (username, password) => {
|
||||
return await users.getByUsernameIfPasswordMatch(username, password);
|
||||
})));
|
||||
|
||||
passport.serializeUser((user, done) => done(null, user.id));
|
||||
passport.deserializeUser((id, done) => nodeifyPromise(users.getById(id), done));
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return done(null, false, {
|
||||
message: _('Incorrect username or password')
|
||||
});
|
||||
}
|
||||
|
||||
return done(null, user);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id);
|
||||
});
|
||||
|
||||
passport.deserializeUser((id, done) => {
|
||||
users.get(id, done);
|
||||
});
|
||||
|
|
|
@ -9,13 +9,14 @@ function replaceLastBySafeHandler(handlers) {
|
|||
|
||||
const lastHandler = handlers[handlers.length - 1];
|
||||
const ret = handlers.slice();
|
||||
ret[handlers.length - 1] = (req, res, next) => lastHandler(req, res).catch(error => next(error));
|
||||
ret[handlers.length - 1] = (req, res, next) => lastHandler(req, res, next).catch(error => next(error));
|
||||
return ret;
|
||||
}
|
||||
|
||||
function create() {
|
||||
const router = new express.Router();
|
||||
|
||||
router.allAsync = (path, ...handlers) => router.all(path, ...replaceLastBySafeHandler(handlers));
|
||||
router.getAsync = (path, ...handlers) => router.get(path, ...replaceLastBySafeHandler(handlers));
|
||||
router.postAsync = (path, ...handlers) => router.post(path, ...replaceLastBySafeHandler(handlers));
|
||||
router.putAsync = (path, ...handlers) => router.put(path, ...replaceLastBySafeHandler(handlers));
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
const _ = require('./translate')._;
|
||||
const util = require('util');
|
||||
const Promise = require('bluebird');
|
||||
const isemail = require('isemail')
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -21,8 +21,6 @@ async function getById(nsId) {
|
|||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
ns.hash = hash(ns);
|
||||
|
||||
return ns;
|
||||
}
|
||||
|
||||
|
|
44
models/settings.js
Normal file
44
models/settings.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
'use strict';
|
||||
|
||||
'use strict';
|
||||
|
||||
const knex = require('../lib/knex');
|
||||
const tools = require('../lib/tools');
|
||||
|
||||
async function get(keyOrKeys) {
|
||||
let keys;
|
||||
if (!Array.isArray(keyOrKeys)) {
|
||||
keys = [ keys ];
|
||||
} else {
|
||||
keys = keyOrKeys;
|
||||
}
|
||||
|
||||
keys = keys.map(key => tools.toDbKey(key));
|
||||
|
||||
const result = await knex('settings').whereIn('key', keys);
|
||||
|
||||
const settings = {};
|
||||
for (const key of keys) {
|
||||
settings[tools.fromDbKey(key)] = result[key];
|
||||
}
|
||||
|
||||
if (!Array.isArray(keyOrKeys)) {
|
||||
return settings[keyOrKeys];
|
||||
} else {
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
async function set(key, value) {
|
||||
try {
|
||||
await knex('settings').insert({key, value});
|
||||
} catch (err) {
|
||||
await knex('settings').where('key', key).update('value', value);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get,
|
||||
set
|
||||
};
|
||||
|
179
models/users.js
179
models/users.js
|
@ -8,31 +8,49 @@ const passwordValidator = require('../shared/password-validator')();
|
|||
const validators = require('../shared/validators');
|
||||
const dtHelpers = require('../lib/dt-helpers');
|
||||
const tools = require('../lib/tools-async');
|
||||
const Promise = require('bluebird');
|
||||
let crypto = require('crypto');
|
||||
const settings = require('./settings');
|
||||
|
||||
const bluebird = require('bluebird');
|
||||
|
||||
const bcrypt = require('bcrypt-nodejs');
|
||||
const bcryptHash = Promise.promisify(bcrypt.hash);
|
||||
const bcryptCompare = Promise.promisify(bcrypt.compare);
|
||||
const bcryptHash = bluebird.promisify(bcrypt.hash);
|
||||
const bcryptCompare = bluebird.promisify(bcrypt.compare);
|
||||
|
||||
const mailer = require('../lib/mailer');
|
||||
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 passport = require('../../lib/passport');
|
||||
|
||||
|
||||
function hash(user) {
|
||||
return hasher.hash(filterObject(user, allowedKeys));
|
||||
}
|
||||
|
||||
async function getById(userId) {
|
||||
const user = await knex('users').select(['id', 'username', 'name', 'email', 'password']).where('id', userId).first();
|
||||
async function _getBy(key, value, extraColumns) {
|
||||
const columns = ['id', 'username', 'name', 'email'];
|
||||
|
||||
if (extraColumns) {
|
||||
columns.push(...extraColumns);
|
||||
}
|
||||
|
||||
const user = await knex('users').select(columns).where(key, value).first();
|
||||
|
||||
if (!user) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
user.hash = hash(user);
|
||||
|
||||
delete(user.password);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function getById(userId) {
|
||||
return await _getBy('id', userId);
|
||||
}
|
||||
|
||||
async function serverValidate(data, isOwnAccount) {
|
||||
const result = {};
|
||||
|
||||
|
@ -123,8 +141,9 @@ async function _validateAndPreprocess(tx, user, isCreate, isOwnAccount) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
async function create(user) {
|
||||
enforce(passport.isAuthMethodLocal, 'Local user management is required');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await _validateAndPreprocess(tx, user, true);
|
||||
const userId = await tx('users').insert(filterObject(user, allowedKeys));
|
||||
|
@ -132,7 +151,16 @@ async function create(user) {
|
|||
});
|
||||
}
|
||||
|
||||
async function createExternal(user) {
|
||||
enforce(!passport.isAuthMethodLocal, 'External user management is required');
|
||||
|
||||
const userId = await knex('users').insert(filterObject(user, allowedKeysExternal));
|
||||
return userId;
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(user, isOwnAccount) {
|
||||
enforce(passport.isAuthMethodLocal, 'Local user management is required');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await _validateAndPreprocess(tx, user, false, isOwnAccount);
|
||||
|
||||
|
@ -147,13 +175,8 @@ async function updateWithConsistencyCheck(user, isOwnAccount) {
|
|||
}
|
||||
|
||||
if (isOwnAccount && user.password) {
|
||||
console.log(user.currentPassword);
|
||||
console.log(existingUser.password);
|
||||
|
||||
if (!await bcryptCompare(user.currentPassword, existingUser.password)) {
|
||||
// This is not an interoperable error because current password is verified in account-validate.
|
||||
// A change of password between account-validate and submit would be signalled by ChangedError
|
||||
throw new Error('Incorrect password');
|
||||
throw new interoperableErrors.IncorrectPasswordError();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,17 +185,139 @@ async function updateWithConsistencyCheck(user, isOwnAccount) {
|
|||
}
|
||||
|
||||
async function remove(userId) {
|
||||
enforce(passport.isAuthMethodLocal, 'Local user management is required');
|
||||
|
||||
// FIXME: enforce that userId is not the current user
|
||||
enforce(userId !== 1, 'Admin cannot be deleted');
|
||||
await knex('users').where('id', userId).del();
|
||||
}
|
||||
|
||||
async function getByAccessToken(accessToken) {
|
||||
return await _getBy('access_token', accessToken);
|
||||
}
|
||||
|
||||
async function getByUsername(username) {
|
||||
return await _getBy('username', username);
|
||||
}
|
||||
|
||||
async function getByUsernameIfPasswordMatch(username, password) {
|
||||
const user = await _getBy('username', username, ['password']);
|
||||
|
||||
if (!await bcryptCompare(password, user.password)) {
|
||||
throw new interoperableErrors.IncorrectPasswordError();
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function getAccessToken(userId) {
|
||||
const user = await _getBy('id', userId, ['access_token']);
|
||||
return user.access_token;
|
||||
}
|
||||
|
||||
async function resetAccessToken(userId) {
|
||||
const token = crypto.randomBytes(20).toString('hex').toLowerCase();
|
||||
|
||||
const affectedRows = await knex('users').where({id: userId}).update({access_token: token});
|
||||
|
||||
if (!affectedRows) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
async function sendPasswordReset(usernameOrEmail) {
|
||||
enforce(passport.isAuthMethodLocal, 'Local user management is required');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
const user = await tx('users').where('username', usernameOrEmail).orWhere('email', usernameOrEmail).select(['id', 'username', 'email', 'name']).first();
|
||||
|
||||
if (user) {
|
||||
const resetToken = crypto.randomBytes(16).toString('base64').replace(/[^a-z0-9]/gi, '');
|
||||
|
||||
await tx('users').where('id', user.id).update({
|
||||
reset_token: resetToken,
|
||||
reset_expire: new Date(Date.now() + 60 * 60 * 1000)
|
||||
});
|
||||
|
||||
const { serviceUrl, adminEmail } = await settings.get(['serviceUrl', 'adminEmail']);
|
||||
|
||||
await mailer.sendMail({
|
||||
from: {
|
||||
address: adminEmail
|
||||
},
|
||||
to: {
|
||||
address: user.email
|
||||
},
|
||||
subject: _('Mailer password change request')
|
||||
}, {
|
||||
html: 'emails/password-reset-html.hbs',
|
||||
text: 'emails/password-reset-text.hbs',
|
||||
data: {
|
||||
title: 'Mailtrain',
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
confirmUrl: urllib.resolve(serviceUrl, `/account/reset-link/${encodeURIComponent(user.username)}/${encodeURIComponent(resetToken)}`)
|
||||
}
|
||||
});
|
||||
}
|
||||
// We intentionally silently ignore the situation when user is not found. This is not to reveal if a user exists in the system.
|
||||
});
|
||||
}
|
||||
|
||||
async function isPasswordResetTokenValid(username, resetToken) {
|
||||
enforce(passport.isAuthMethodLocal, 'Local user management is required');
|
||||
|
||||
const user = await knex('users').select(['id']).where({username, reset_token: resetToken}).andWhere('reset_expire', '>', new Date()).first();
|
||||
return !!user;
|
||||
}
|
||||
|
||||
async function resetPassword(username, resetToken, password) {R
|
||||
enforce(passport.isAuthMethodLocal, 'Local user management is required');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
const user = await tx('users').select(['id']).where({
|
||||
username,
|
||||
reset_token: resetToken
|
||||
}).andWhere('reset_expire', '>', new Date()).first();
|
||||
|
||||
if (user) {
|
||||
const passwordValidatorResults = passwordValidator.test(password);
|
||||
if (passwordValidatorResults.errors.length > 0) {
|
||||
// This is not an interoperable error because this is not supposed to happen unless the client is tampered with.
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
|
||||
password = await bcryptHash(password, null, null);
|
||||
|
||||
await tx('users').where({username}).update({
|
||||
password,
|
||||
reset_token: null,
|
||||
reset_expire: null
|
||||
});
|
||||
} else {
|
||||
throw new interoperableErrors.InvalidToken();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
listDTAjax,
|
||||
remove,
|
||||
updateWithConsistencyCheck,
|
||||
create,
|
||||
createExternal,
|
||||
hash,
|
||||
getById,
|
||||
serverValidate
|
||||
serverValidate,
|
||||
getByAccessToken,
|
||||
getByUsername,
|
||||
getByUsernameIfPasswordMatch,
|
||||
getAccessToken,
|
||||
resetAccessToken,
|
||||
sendPasswordReset,
|
||||
isPasswordResetTokenValid,
|
||||
resetPassword
|
||||
};
|
|
@ -97,6 +97,7 @@
|
|||
"node-gettext": "^2.0.0-rc.1",
|
||||
"node-mocks-http": "^1.6.1",
|
||||
"node-object-hash": "^1.2.0",
|
||||
"nodeify": "^1.0.1",
|
||||
"nodemailer": "^4.0.1",
|
||||
"nodemailer-openpgp": "^1.0.2",
|
||||
"npmlog": "^4.0.2",
|
||||
|
|
15
routes/account-legacy-integration.js
Normal file
15
routes/account-legacy-integration.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
router.get('/logout', (req, res) => {
|
||||
req.logout();
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
clientHelpers.registerRootRoute(router, _('Account'), 'account');
|
||||
|
||||
module.exports = router;
|
|
@ -1,59 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('../lib/passport');
|
||||
const router = require('../lib/router-async').create();
|
||||
const _ = require('../lib/translate')._;
|
||||
const users = require('../models/users');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
|
||||
|
||||
router.all('/rest/*', (req, res, next) => {
|
||||
req.needsJSONResponse = true;
|
||||
|
||||
if (!req.user) {
|
||||
throw new interoperableErrors.NotLoggedInError();
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
router.getAsync('/rest/account', async (req, res) => {
|
||||
const user = await users.getById(req.user.id);
|
||||
return res.json(user);
|
||||
});
|
||||
|
||||
router.postAsync('/rest/account', passport.csrfProtection, async (req, res) => {
|
||||
const data = req.body;
|
||||
data.id = req.user.id;
|
||||
|
||||
await users.updateWithConsistencyCheck(req.body, true);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.postAsync('/rest/account-validate', async (req, res) => {
|
||||
const data = req.body;
|
||||
data.id = req.user.id;
|
||||
|
||||
return res.json(await users.serverValidate(data, true));
|
||||
});
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
// res.setSelectedMenu('users'); FIXME
|
||||
next();
|
||||
});
|
||||
|
||||
router.getAsync('/*', passport.csrfProtection, async (req, res) => {
|
||||
res.render('react-root', {
|
||||
title: _('Account'),
|
||||
reactEntryPoint: 'account',
|
||||
reactCsrfToken: req.csrfToken()
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -1,18 +1,18 @@
|
|||
'use strict';
|
||||
|
||||
let users = require('../lib/models/users-legacy');
|
||||
let users = require('../models/users');
|
||||
let lists = require('../lib/models/lists');
|
||||
let fields = require('../lib/models/fields');
|
||||
let blacklist = require('../lib/models/blacklist');
|
||||
let subscriptions = require('../lib/models/subscriptions');
|
||||
let confirmations = require('../lib/models/confirmations');
|
||||
let tools = require('../lib/tools');
|
||||
let express = require('express');
|
||||
let log = require('npmlog');
|
||||
let router = new express.Router();
|
||||
const router = require('../lib/router-async').create();
|
||||
let mailHelpers = require('../lib/subscription-mail-helpers');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
router.allAsync('/*', async (req, res, next) => {
|
||||
if (!req.query.access_token) {
|
||||
res.status(403);
|
||||
return res.json({
|
||||
|
@ -21,24 +21,24 @@ router.all('/*', (req, res, next) => {
|
|||
});
|
||||
}
|
||||
|
||||
users.findByAccessToken(req.query.access_token, (err, user) => {
|
||||
if (err) {
|
||||
try {
|
||||
await users.getByAccessToken(req.query.access_token);
|
||||
next();
|
||||
} catch (err) {
|
||||
if (err instanceof interoperableErrors.NotFoundError) {
|
||||
res.status(403);
|
||||
return res.json({
|
||||
error: 'Invalid or expired access_token',
|
||||
data: []
|
||||
});
|
||||
} else {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
if (!user) {
|
||||
res.status(403);
|
||||
return res.json({
|
||||
error: 'Invalid or expired access_token',
|
||||
data: []
|
||||
});
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
router.post('/subscribe/:listId', (req, res) => {
|
||||
|
|
10
routes/namespaces-legacy-integration.js
Normal file
10
routes/namespaces-legacy-integration.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
clientHelpers.registerRootRoute(router, _('Namespaces'), 'namespaces');
|
||||
|
||||
module.exports = router;
|
|
@ -1,95 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('../lib/passport');
|
||||
const router = require('../lib/router-async').create();
|
||||
const _ = require('../lib/translate')._;
|
||||
const namespaces = require('../models/namespaces');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
|
||||
router.all('/rest/*', (req, res, next) => {
|
||||
req.needsJSONResponse = true;
|
||||
|
||||
if (!req.user) {
|
||||
throw new interoperableErrors.NotLoggedInError();
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
router.getAsync('/rest/namespaces/:nsId', async (req, res) => {
|
||||
const ns = await namespaces.getById(req.params.nsId);
|
||||
return res.json(ns);
|
||||
});
|
||||
|
||||
router.postAsync('/rest/namespaces', passport.csrfProtection, async (req, res) => {
|
||||
await namespaces.create(req.body);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.putAsync('/rest/namespaces/:nsId', passport.csrfProtection, async (req, res) => {
|
||||
const ns = req.body;
|
||||
ns.id = parseInt(req.params.nsId);
|
||||
|
||||
await namespaces.updateWithConsistencyCheck(ns);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.deleteAsync('/rest/namespaces/:nsId', passport.csrfProtection, async (req, res) => {
|
||||
await namespaces.remove(req.params.nsId);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.getAsync('/rest/namespaces-tree', async (req, res) => {
|
||||
const entries = {};
|
||||
let root; // Only the Root namespace is without a parent
|
||||
const rows = await namespaces.list();
|
||||
|
||||
for (let row of rows) {
|
||||
let entry;
|
||||
if (!entries[row.id]) {
|
||||
entry = {
|
||||
children: []
|
||||
};
|
||||
entries[row.id] = entry;
|
||||
} else {
|
||||
entry = entries[row.id];
|
||||
}
|
||||
|
||||
if (row.parent) {
|
||||
if (!entries[row.parent]) {
|
||||
entries[row.parent] = {
|
||||
children: []
|
||||
};
|
||||
}
|
||||
|
||||
entries[row.parent].children.push(entry);
|
||||
|
||||
} else {
|
||||
root = entry;
|
||||
}
|
||||
|
||||
entry.title = row.name;
|
||||
entry.key = row.id;
|
||||
}
|
||||
|
||||
return res.json(root);
|
||||
});
|
||||
|
||||
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));
|
||||
}
|
||||
// res.setSelectedMenu('namespaces'); FIXME
|
||||
next();
|
||||
});
|
||||
|
||||
router.getAsync('/*', passport.csrfProtection, async (req, res) => {
|
||||
res.render('react-root', {
|
||||
title: _('Namespaces'),
|
||||
reactEntryPoint: 'namespaces',
|
||||
reactCsrfToken: req.csrfToken()
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
61
routes/rest/account.js
Normal file
61
routes/rest/account.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('../../lib/passport');
|
||||
const _ = require('../../lib/translate')._;
|
||||
const users = require('../../models/users');
|
||||
|
||||
const router = require('../../lib/router-async').create();
|
||||
|
||||
|
||||
router.getAsync('/account', passport.loggedIn, async (req, res) => {
|
||||
const user = await users.getById(req.user.id);
|
||||
return res.json(user);
|
||||
});
|
||||
|
||||
router.postAsync('/account', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const data = req.body;
|
||||
data.id = req.user.id;
|
||||
|
||||
await users.updateWithConsistencyCheck(req.body, true);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.postAsync('/account-validate', passport.loggedIn, async (req, res) => {
|
||||
const data = req.body;
|
||||
data.id = req.user.id;
|
||||
|
||||
return res.json(await users.serverValidate(data, true));
|
||||
});
|
||||
|
||||
router.getAsync('/access-token', passport.loggedIn, async (req, res) => {
|
||||
const accessToken = await users.getAccessToken(req.user.id);
|
||||
return res.json(accessToken);
|
||||
|
||||
});
|
||||
|
||||
router.postAsync('/access-token-reset', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const accessToken = await users.resetAccessToken(req.user.id);
|
||||
return res.json(accessToken);
|
||||
});
|
||||
|
||||
|
||||
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.postAsync('/password-reset-send', async (req, res) => {
|
||||
await users.sendPasswordReset(req.body.username);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.getAsync('/password-reset-validate', 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) => {
|
||||
await users.resetPassword(req.body.username, req.body.resetToken, req.body.password);
|
||||
return res.json();
|
||||
})
|
||||
|
||||
|
||||
module.exports = router;
|
74
routes/rest/namespaces.js
Normal file
74
routes/rest/namespaces.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
'use strict';
|
||||
|
||||
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();
|
||||
|
||||
|
||||
router.getAsync('/namespaces/:nsId', passport.loggedIn, async (req, res) => {
|
||||
const ns = await namespaces.getById(req.params.nsId);
|
||||
|
||||
ns.hash = namespaces.hash(ns);
|
||||
|
||||
return res.json(ns);
|
||||
});
|
||||
|
||||
router.postAsync('/namespaces', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await namespaces.create(req.body);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.putAsync('/namespaces/:nsId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const ns = req.body;
|
||||
ns.id = parseInt(req.params.nsId);
|
||||
|
||||
await namespaces.updateWithConsistencyCheck(ns);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.deleteAsync('/namespaces/:nsId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await namespaces.remove(req.params.nsId);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.getAsync('/namespaces-tree', passport.loggedIn, async (req, res) => {
|
||||
const entries = {};
|
||||
let root; // Only the Root namespace is without a parent
|
||||
const rows = await namespaces.list();
|
||||
|
||||
for (let row of rows) {
|
||||
let entry;
|
||||
if (!entries[row.id]) {
|
||||
entry = {
|
||||
children: []
|
||||
};
|
||||
entries[row.id] = entry;
|
||||
} else {
|
||||
entry = entries[row.id];
|
||||
}
|
||||
|
||||
if (row.parent) {
|
||||
if (!entries[row.parent]) {
|
||||
entries[row.parent] = {
|
||||
children: []
|
||||
};
|
||||
}
|
||||
|
||||
entries[row.parent].children.push(entry);
|
||||
|
||||
} else {
|
||||
root = entry;
|
||||
}
|
||||
|
||||
entry.title = row.name;
|
||||
entry.key = row.id;
|
||||
}
|
||||
|
||||
return res.json(root);
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
44
routes/rest/users.js
Normal file
44
routes/rest/users.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
'use strict';
|
||||
|
||||
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();
|
||||
|
||||
|
||||
router.getAsync('/users/:userId', passport.loggedIn, async (req, res) => {
|
||||
const user = await users.getById(req.params.userId);
|
||||
user.hash = users.hash(user);
|
||||
return res.json(user);
|
||||
});
|
||||
|
||||
router.postAsync('/users', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await users.create(req.body);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.putAsync('/users/:userId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const user = req.body;
|
||||
user.id = parseInt(req.params.userId);
|
||||
|
||||
await users.updateWithConsistencyCheck(user);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.deleteAsync('/users/:userId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await users.remove(req.params.userId);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.postAsync('/users-validate', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await users.serverValidate(req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/users-table', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await users.listDTAjax(req.body));
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -3,7 +3,7 @@
|
|||
let passport = require('../lib/passport');
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let users = require('../lib/models/users-legacy');
|
||||
let users = require('../lib/models/users-legacy-REMOVE');
|
||||
let fields = require('../lib/models/fields');
|
||||
let settings = require('../lib/models/settings');
|
||||
let _ = require('../lib/translate')._;
|
||||
|
@ -115,33 +115,4 @@ router.post('/api/reset-token', passport.parseForm, passport.csrfProtection, (re
|
|||
});
|
||||
});
|
||||
|
||||
router.all('/account', (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('/account', passport.csrfProtection, (req, res) => {
|
||||
let data = {
|
||||
csrfToken: req.csrfToken(),
|
||||
email: req.user.email
|
||||
};
|
||||
res.render('users/account', data);
|
||||
});
|
||||
|
||||
router.post('/account', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
users.update(Number(req.user.id), req.body, (err, success) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
} else if (success) {
|
||||
req.flash('success', _('Account information updated'));
|
||||
} else {
|
||||
req.flash('info', _('Account information not updated'));
|
||||
}
|
||||
return res.redirect('/users/account');
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
10
routes/users-legacy-integration.js
Normal file
10
routes/users-legacy-integration.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
clientHelpers.registerRootRoute(router, _('Users'), 'users');
|
||||
|
||||
module.exports = router;
|
|
@ -1,70 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('../lib/passport');
|
||||
const router = require('../lib/router-async').create();
|
||||
const _ = require('../lib/translate')._;
|
||||
const users = require('../models/users');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
|
||||
|
||||
router.all('/rest/*', (req, res, next) => {
|
||||
req.needsJSONResponse = true;
|
||||
|
||||
if (!req.user) {
|
||||
throw new interoperableErrors.NotLoggedInError();
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
router.getAsync('/rest/users/:userId', async (req, res) => {
|
||||
const user = await users.getById(req.params.userId);
|
||||
return res.json(user);
|
||||
});
|
||||
|
||||
router.postAsync('/rest/users', passport.csrfProtection, async (req, res) => {
|
||||
await users.create(req.body);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.putAsync('/rest/users/:userId', passport.csrfProtection, async (req, res) => {
|
||||
const user = req.body;
|
||||
user.id = parseInt(req.params.userId);
|
||||
|
||||
await users.updateWithConsistencyCheck(user);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.deleteAsync('/rest/users/:userId', passport.csrfProtection, async (req, res) => {
|
||||
await users.remove(req.params.userId);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.postAsync('/rest/users-validate', async (req, res) => {
|
||||
return res.json(await users.serverValidate(req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/rest/users-table', async (req, res) => {
|
||||
return res.json(await users.listDTAjax(req.body));
|
||||
});
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
// res.setSelectedMenu('users'); FIXME
|
||||
next();
|
||||
});
|
||||
|
||||
router.getAsync('/*', passport.csrfProtection, async (req, res) => {
|
||||
res.render('react-root', {
|
||||
title: _('Users'),
|
||||
reactEntryPoint: 'users',
|
||||
reactCsrfToken: req.csrfToken()
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -1,6 +1,8 @@
|
|||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table('users', table => {
|
||||
table.string('name').notNullable().default('');
|
||||
// name and password can be null in case of LDAP login
|
||||
table.string('name');
|
||||
table.string('password').alter();
|
||||
})
|
||||
.then(() => knex('users').where('id', 1).update({
|
||||
name: 'Administrator'
|
||||
|
|
|
@ -50,6 +50,18 @@ class DuplicitEmailError extends InteroperableError {
|
|||
}
|
||||
}
|
||||
|
||||
class IncorrectPasswordError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('IncorrectPasswordError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidToken extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('InvalidToken', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
const errorTypes = {
|
||||
InteroperableError,
|
||||
NotLoggedInError,
|
||||
|
@ -58,7 +70,9 @@ const errorTypes = {
|
|||
LoopDetectedError,
|
||||
ChildDetectedError,
|
||||
DuplicitNameError,
|
||||
DuplicitEmailError
|
||||
DuplicitEmailError,
|
||||
IncorrectPasswordError,
|
||||
InvalidToken
|
||||
};
|
||||
|
||||
function deserialize(errorObj) {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const Promise = require('bluebird');
|
||||
|
||||
class WorkerCounter {
|
||||
constructor() {
|
||||
this.counter = 0;
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
<script>
|
||||
window.csfrToken = '{{reactCsrfToken}}';
|
||||
window.mailtrainConfig = {{mailtrainConfig}};
|
||||
</script>
|
||||
|
||||
<script src="/mailtrain/{{reactEntryPoint}}.js"></script>
|
||||
|
@ -78,19 +79,12 @@
|
|||
<li><a href="https://mailtrain.wordpress.com/"><span class="glyphicon glyphicon-share-alt" aria-hidden="true"></span> {{#translate}}Blog{{/translate}}</a></li>
|
||||
</ul>
|
||||
|
||||
{{#if user }}
|
||||
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{{#if admin }}
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-user" aria-hidden="true"></span> {{user.username}} <span class="caret"></span>
|
||||
</a>
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Administration<span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/users/account">
|
||||
<span class="glyphicon glyphicon-user" aria-hidden="true"></span> {{#translate}}Account{{/translate}}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/settings">
|
||||
<span class="glyphicon glyphicon-cog" aria-hidden="true"></span> {{#translate}}Settings{{/translate}}
|
||||
|
@ -102,12 +96,30 @@
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/users/api">
|
||||
<a href="/account/api">
|
||||
<span class="glyphicon glyphicon-retweet" aria-hidden="true"></span> {{#translate}}API{{/translate}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if user }}
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-user" aria-hidden="true"></span> {{user.username}} <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="/users/logout">
|
||||
<a href="/account">
|
||||
<span class="glyphicon glyphicon-user" aria-hidden="true"></span> {{#translate}}Account{{/translate}}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/account/logout">
|
||||
<span class="glyphicon glyphicon-log-out" aria-hidden="true"></span> {{#translate}}Log out{{/translate}}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -119,7 +131,7 @@
|
|||
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<a href="/users/login" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<a href="/account/login" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-log-in" aria-hidden="true"></span> {{#translate}}Sign in{{/translate}}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
<ol class="breadcrumb">
|
||||
<li><a href="/">{{#translate}}Home{{/translate}}</a></li>
|
||||
<li class="active">{{#translate}}Account{{/translate}}</li>
|
||||
</ol>
|
||||
|
||||
<h2>{{#translate}}Account{{/translate}}</h2>
|
||||
|
||||
<hr>
|
||||
|
||||
{{#if ldap.enabled}}
|
||||
<p>
|
||||
{{#translate}}This account is managed through LDAP.{{/translate}}
|
||||
<br/>
|
||||
<br/> {{#translate}}Associated Email Address{{/translate}}: <a href="mailto:{{email}}">{{email}}</a>
|
||||
</p>
|
||||
{{else}}
|
||||
<form class="form-horizontal" method="post" action="/users/account">
|
||||
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
|
||||
<fieldset>
|
||||
<legend>
|
||||
{{#translate}}General Settings{{/translate}}
|
||||
</legend>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email" class="col-sm-2 control-label">{{#translate}}Email Address{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" class="form-control" name="email" id="email" value="{{email}}" placeholder="{{#translate}}Your e-mail address{{/translate}}" required>
|
||||
<span class="help-block">{{#translate}}This address is used for account recovery in case you loose your password{{/translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>
|
||||
{{#translate}}Password change{{/translate}}
|
||||
</legend>
|
||||
|
||||
<p>
|
||||
{{#translate}}You only need to fill out this form if you want to change your current password{{/translate}}
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="currentPassword" class="col-sm-2 control-label">{{#translate}}Current Password{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" name="currentPassword" id="currentPassword" placeholder="{{#translate}}Current Password{{/translate}}">
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary"><i class="glyphicon glyphicon-ok"></i> {{#translate}}Update{{/translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{{/if}}
|
Loading…
Add table
Add a link
Reference in a new issue