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
|
@ -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,53 +103,91 @@ export default class Account extends Component {
|
|||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Updating user profile ...'));
|
||||
try {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Updating user profile ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/account/rest/account', data => {
|
||||
delete data.password2;
|
||||
});
|
||||
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();
|
||||
this.updateFormValue('password', '');
|
||||
this.updateFormValue('password2', '');
|
||||
this.updateFormValue('currentPassword', '');
|
||||
|
||||
if (submitSuccessful) {
|
||||
this.setFlashMessage('success', t('User profile updated'));
|
||||
this.hideFormValidation();
|
||||
this.updateFormValue('password', '');
|
||||
this.updateFormValue('password2', '');
|
||||
this.updateFormValue('currentPassword', '');
|
||||
this.clearFormStatusMessage();
|
||||
} 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.clearFormStatusMessage();
|
||||
} else {
|
||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||
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;
|
||||
|
||||
return (
|
||||
if (mailtrainConfig.isAuthMethodLocal) {
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Account')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<Fieldset label={t('General Settings')}>
|
||||
<InputField id="name" label={t('Full Name')}/>
|
||||
<InputField id="email" label={t('Email')} help={t('This address is used for account recovery in case you loose your password')}/>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset label={t('Password Change')}>
|
||||
<p>{t('You only need to fill out this form if you want to change your current password')}</p>
|
||||
<InputField id="currentPassword" label={t('Current Password')} type="password" />
|
||||
<InputField id="password" label={t('New Password')} type="password" />
|
||||
<InputField id="password2" label={t('Confirm Password')} type="password" />
|
||||
</Fieldset>
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Update')}/>
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
<div>
|
||||
<Title>{t('Account')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<Fieldset label={t('General Settings')}>
|
||||
<InputField id="name" label={t('Full Name')}/>
|
||||
<InputField id="email" label={t('Email')} help={t('This address is used for account recovery in case you loose your password')}/>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset label={t('Password Change')}>
|
||||
<p>{t('You only need to fill out this form if you want to change your current password')}</p>
|
||||
<InputField id="currentPassword" label={t('Current Password')} type="password" />
|
||||
<InputField id="password" label={t('New Password')} type="password" />
|
||||
<InputField id="password2" label={t('Confirm Password')} type="password" />
|
||||
</Fieldset>
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Update')}/>
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
<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 => ({
|
||||
'': {
|
||||
title: t('Home'),
|
||||
externalLink: '/',
|
||||
children: {
|
||||
'account': {
|
||||
title: t('Account'),
|
||||
link: '/account',
|
||||
component: Account
|
||||
|
||||
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: {
|
||||
title: t('Account'),
|
||||
link: '/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);
|
||||
}
|
||||
}
|
||||
}));
|
||||
})
|
||||
}));
|
||||
|
||||
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.scheduleFormRevalidate = function() {
|
||||
scheduleValidateForm(this);
|
||||
};
|
||||
|
||||
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 => ({
|
||||
'': {
|
||||
title: t('Home'),
|
||||
externalLink: '/',
|
||||
children: {
|
||||
'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} />)
|
||||
}
|
||||
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: {
|
||||
title: t('Users'),
|
||||
link: '/users',
|
||||
component: List,
|
||||
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(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue