mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 02:43:46 +08:00
Merge branch 'master' into onbuild
This commit is contained in:
+31
-1
@@ -241,13 +241,21 @@ function listUsers() {
|
||||
});
|
||||
|
||||
users.forEach((user) => {
|
||||
let state = user.disabled ? 'Disabled' : 'Enabled';
|
||||
const profile = user.profiles.find(({provider}) => provider === 'local');
|
||||
if (profile && profile.metadata && profile.metadata.confirmed_at) {
|
||||
state += ', Verified';
|
||||
} else {
|
||||
state += ', Unverified';
|
||||
}
|
||||
|
||||
table.push([
|
||||
user.id,
|
||||
user.username,
|
||||
user.profiles.map((p) => p.provider).join(', '),
|
||||
user.roles.join(', '),
|
||||
user.status,
|
||||
user.disabled ? 'Disabled' : 'Enabled'
|
||||
state
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -396,6 +404,23 @@ function enableUser(userID) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies an email address for a user.
|
||||
*
|
||||
* @param userID the user's id
|
||||
* @param email the user's email address to be verified
|
||||
*/
|
||||
async function verify(userID, email) {
|
||||
try {
|
||||
await UsersService.confirmEmail(userID, email);
|
||||
console.log(`User ${userID} had their email ${email} verified.`);
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
@@ -467,6 +492,11 @@ program
|
||||
.description('enable a given user from logging in')
|
||||
.action(enableUser);
|
||||
|
||||
program
|
||||
.command('verify <userID> <email>')
|
||||
.description('verifies the given user\'s email address')
|
||||
.action(verify);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ test:
|
||||
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
|
||||
# Check dependancies using nsp.
|
||||
- nsp check
|
||||
- yarn e2e-ci
|
||||
# - yarn e2e-ci
|
||||
|
||||
deployment:
|
||||
release:
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import queryString from 'query-string';
|
||||
|
||||
import {
|
||||
FETCH_COMMENTERS_REQUEST,
|
||||
FETCH_COMMENTERS_SUCCESS,
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
FETCH_USERS_REQUEST,
|
||||
FETCH_USERS_SUCCESS,
|
||||
FETCH_USERS_FAILURE,
|
||||
SORT_UPDATE,
|
||||
COMMENTERS_NEW_PAGE,
|
||||
SET_PAGE,
|
||||
SET_SEARCH_VALUE,
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
@@ -16,13 +17,13 @@ import {
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => {
|
||||
dispatch(requestFetchAccounts());
|
||||
export const fetchUsers = (query = {}) => (dispatch, _, {rest}) => {
|
||||
dispatch(requestFetchUsers());
|
||||
rest(`/users?${queryString.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>{
|
||||
dispatch({
|
||||
type: FETCH_COMMENTERS_SUCCESS,
|
||||
accounts: result,
|
||||
type: FETCH_USERS_SUCCESS,
|
||||
users: result,
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
@@ -32,12 +33,12 @@ export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: FETCH_COMMENTERS_FAILURE, error: errorMessage});
|
||||
dispatch({type: FETCH_USERS_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
const requestFetchAccounts = () => ({
|
||||
type: FETCH_COMMENTERS_REQUEST
|
||||
const requestFetchUsers = () => ({
|
||||
type: FETCH_USERS_REQUEST
|
||||
});
|
||||
|
||||
export const updateSorting = (sort) => ({
|
||||
@@ -45,8 +46,14 @@ export const updateSorting = (sort) => ({
|
||||
sort
|
||||
});
|
||||
|
||||
export const newPage = () => ({
|
||||
type: COMMENTERS_NEW_PAGE
|
||||
export const setPage = (page) => ({
|
||||
type: SET_PAGE,
|
||||
page,
|
||||
});
|
||||
|
||||
export const setSearchValue = (value) => ({
|
||||
type: SET_SEARCH_VALUE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const setRole = (id, role) => (dispatch, _, {rest}) => {
|
||||
|
||||
+29
-5
@@ -1,12 +1,17 @@
|
||||
import queryString from 'query-string';
|
||||
|
||||
import {
|
||||
FETCH_ASSETS_REQUEST,
|
||||
FETCH_ASSETS_SUCCESS,
|
||||
FETCH_ASSETS_FAILURE,
|
||||
SET_PAGE,
|
||||
SET_SEARCH_VALUE,
|
||||
SET_CRITERIA,
|
||||
UPDATE_ASSET_STATE_REQUEST,
|
||||
UPDATE_ASSET_STATE_SUCCESS,
|
||||
UPDATE_ASSET_STATE_FAILURE,
|
||||
UPDATE_ASSETS
|
||||
} from '../constants/assets';
|
||||
} from '../constants/stories';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -16,13 +21,16 @@ import t from 'coral-framework/services/i18n';
|
||||
|
||||
// Fetch a page of assets
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch, _, {rest}) => {
|
||||
export const fetchAssets = (query = {}) => (dispatch, _, {rest}) => {
|
||||
dispatch({type: FETCH_ASSETS_REQUEST});
|
||||
return rest(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
|
||||
.then(({result, count}) =>
|
||||
return rest(`/assets?${queryString.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>
|
||||
dispatch({type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
count
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
totalPages,
|
||||
}))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
@@ -47,3 +55,19 @@ export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => {
|
||||
export const updateAssets = (assets) => (dispatch) => {
|
||||
dispatch({type: UPDATE_ASSETS, assets});
|
||||
};
|
||||
|
||||
export const setPage = (page) => ({
|
||||
type: SET_PAGE,
|
||||
page,
|
||||
});
|
||||
|
||||
export const setSearchValue = (value) => ({
|
||||
type: SET_SEARCH_VALUE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const setCriteria = (criteria) => ({
|
||||
type: SET_CRITERIA,
|
||||
criteria,
|
||||
});
|
||||
|
||||
@@ -44,7 +44,6 @@ export default class UserDetail extends React.Component {
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
@@ -56,7 +55,6 @@ export default class UserDetail extends React.Component {
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
@@ -68,7 +66,6 @@ export default class UserDetail extends React.Component {
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
@@ -80,7 +77,6 @@ export default class UserDetail extends React.Component {
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
|
||||
import {Link, IndexLink} from 'react-router';
|
||||
@@ -24,7 +25,7 @@ const CoralHeader = ({
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
className={styles.navLink}
|
||||
className={cn('talk-admin-nav-dashboard', styles.navLink)}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.dashboard')}
|
||||
@@ -33,7 +34,7 @@ const CoralHeader = ({
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
id='moderateNav'
|
||||
className={styles.navLink}
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
@@ -42,8 +43,8 @@ const CoralHeader = ({
|
||||
)
|
||||
}
|
||||
<Link
|
||||
id='streamsNav'
|
||||
className={styles.navLink}
|
||||
id='storiesNav'
|
||||
className={cn('talk-admin-nav-stories', styles.navLink)}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.stories')}
|
||||
@@ -51,7 +52,7 @@ const CoralHeader = ({
|
||||
|
||||
<Link
|
||||
id='communityNav'
|
||||
className={styles.navLink}
|
||||
className={cn('talk-admin-nav-community', styles.navLink)}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.community')}
|
||||
@@ -62,7 +63,7 @@ const CoralHeader = ({
|
||||
can(auth.user, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={styles.navLink}
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export const FETCH_ASSETS_REQUEST = 'FETCH_ASSETS_REQUEST';
|
||||
export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS';
|
||||
export const FETCH_ASSETS_FAILURE = 'FETCH_ASSETS_FAILURE';
|
||||
|
||||
export const UPDATE_ASSET_STATE_REQUEST = 'UPDATE_ASSET_STATE_REQUEST';
|
||||
export const UPDATE_ASSET_STATE_SUCCESS = 'UPDATE_ASSET_STATE_SUCCESS';
|
||||
export const UPDATE_ASSET_STATE_FAILURE = 'UPDATE_ASSET_STATE_FAILURE';
|
||||
|
||||
export const UPDATE_ASSETS = 'UPDATE_ASSETS';
|
||||
@@ -1,17 +1,22 @@
|
||||
export const FETCH_COMMENTERS_REQUEST = 'FETCH_COMMENTERS_REQUEST';
|
||||
export const FETCH_COMMENTERS_SUCCESS = 'FETCH_COMMENTERS_SUCCESS';
|
||||
export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE';
|
||||
export const SORT_UPDATE = 'SORT_UPDATE';
|
||||
export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE';
|
||||
export const SET_ROLE = 'SET_ROLE';
|
||||
export const SET_COMMENTER_STATUS = 'SET_COMMENTER_STATUS';
|
||||
const prefix = 'COMMUNITY';
|
||||
|
||||
export const FETCH_FLAGGED_COMMENTERS_REQUEST = 'FETCH_FLAGGED_COMMENTERS_REQUEST';
|
||||
export const FETCH_FLAGGED_COMMENTERS_SUCCESS = 'FETCH_FLAGGED_COMMENTERS_SUCCESS';
|
||||
export const FETCH_FLAGGED_COMMENTERS_FAILURE = 'FETCH_FLAGGED_COMMENTERS_FAILURE';
|
||||
export const FETCH_USERS_REQUEST = `${prefix}_FETCH_USERS_REQUEST`;
|
||||
export const FETCH_USERS_SUCCESS = `${prefix}_FETCH_USERS_SUCCESS`;
|
||||
export const FETCH_USERS_FAILURE = `${prefix}_FETCH_USERS_FAILURE`;
|
||||
|
||||
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||
export const SORT_UPDATE = `${prefix}_SORT_UPDATE`;
|
||||
export const SET_PAGE = `${prefix}_SET_PAGE`;
|
||||
export const SET_ROLE = `${prefix}_SET_ROLE`;
|
||||
export const SET_COMMENTER_STATUS = `${prefix}_SET_COMMENTER_STATUS`;
|
||||
|
||||
export const SHOW_REJECT_USERNAME_DIALOG = 'SHOW_REJECT_USERNAME_DIALOG';
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = 'HIDE_REJECT_USERNAME_DIALOG';
|
||||
export const FETCH_FLAGGED_COMMENTERS_REQUEST = `${prefix}_FETCH_FLAGGED_COMMENTERS_REQUEST`;
|
||||
export const FETCH_FLAGGED_COMMENTERS_SUCCESS = `${prefix}_FETCH_FLAGGED_COMMENTERS_SUCCESS`;
|
||||
export const FETCH_FLAGGED_COMMENTERS_FAILURE = `${prefix}_FETCH_FLAGGED_COMMENTERS_FAILURE`;
|
||||
|
||||
export const SHOW_BANUSER_DIALOG = `${prefix}_SHOW_BANUSER_DIALOG`;
|
||||
export const HIDE_BANUSER_DIALOG = `${prefix}_HIDE_BANUSER_DIALOG`;
|
||||
|
||||
export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALOG`;
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`;
|
||||
|
||||
export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
const prefix = 'STORIES';
|
||||
|
||||
export const FETCH_ASSETS_REQUEST = `${prefix}_FETCH_ASSETS_REQUEST`;
|
||||
export const FETCH_ASSETS_SUCCESS = `${prefix}_FETCH_ASSETS_SUCCESS`;
|
||||
export const FETCH_ASSETS_FAILURE = `${prefix}_FETCH_ASSETS_FAILURE`;
|
||||
|
||||
export const UPDATE_ASSET_STATE_REQUEST = `${prefix}_UPDATE_ASSET_STATE_REQUEST`;
|
||||
export const UPDATE_ASSET_STATE_SUCCESS = `${prefix}_UPDATE_ASSET_STATE_SUCCESS`;
|
||||
export const UPDATE_ASSET_STATE_FAILURE = `${prefix}_UPDATE_ASSET_STATE_FAILURE`;
|
||||
|
||||
export const UPDATE_ASSETS = `${prefix}_UPDATE_ASSETS`;
|
||||
|
||||
export const SET_PAGE = `${prefix}_SET_PAGE`;
|
||||
export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`;
|
||||
export const SET_CRITERIA = `${prefix}_SET_CRITERIA`;
|
||||
@@ -43,22 +43,16 @@ class UserDetailContainer extends React.Component {
|
||||
return this.props.setCommentStatus({commentId, status});
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(changes);
|
||||
this.props.clearUserDetailSelections(); // un-select everything
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
}
|
||||
await Promise.all(changes);
|
||||
this.props.clearUserDetailSelections(); // un-select everything
|
||||
}
|
||||
|
||||
bulkReject = () => {
|
||||
this.bulkSetCommentStatus('REJECTED');
|
||||
return this.bulkSetCommentStatus('REJECTED');
|
||||
}
|
||||
|
||||
bulkAccept = () => {
|
||||
this.bulkSetCommentStatus('ACCEPTED');
|
||||
return this.bulkSetCommentStatus('ACCEPTED');
|
||||
}
|
||||
|
||||
acceptComment = ({commentId}) => {
|
||||
@@ -171,7 +165,8 @@ export const withUserDetailQuery = withQuery(gql`
|
||||
`, {
|
||||
options: ({userId, statuses}) => {
|
||||
return {
|
||||
variables: {author_id: userId, statuses}
|
||||
variables: {author_id: userId, statuses},
|
||||
fetchPolicy: 'network-only',
|
||||
};
|
||||
},
|
||||
skip: (ownProps) => !ownProps.userId,
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import * as actions from '../constants/assets';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
const initialState = {
|
||||
byId: {},
|
||||
ids: [],
|
||||
assets: []
|
||||
};
|
||||
|
||||
export default function assets (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FETCH_ASSETS_SUCCESS: {
|
||||
const assets = action.assets.reduce((prev, curr) => {
|
||||
prev[curr.id] = curr;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
return update(state, {
|
||||
byId: {$set: assets},
|
||||
count: {$set: action.count},
|
||||
ids: {$set: Object.keys(assets)},
|
||||
});
|
||||
}
|
||||
case actions.UPDATE_ASSET_STATE_REQUEST:
|
||||
return update(state, {
|
||||
byId: {
|
||||
[action.id]: {
|
||||
closedAt: {$set: action.closedAt},
|
||||
},
|
||||
},
|
||||
});
|
||||
case actions.UPDATE_ASSETS:
|
||||
return update(state, {
|
||||
assets: {$set: action.assets},
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
FETCH_COMMENTERS_REQUEST,
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
FETCH_COMMENTERS_SUCCESS,
|
||||
FETCH_USERS_REQUEST,
|
||||
FETCH_USERS_SUCCESS,
|
||||
FETCH_USERS_FAILURE,
|
||||
SORT_UPDATE,
|
||||
SET_PAGE,
|
||||
SET_SEARCH_VALUE,
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
@@ -15,7 +17,8 @@ const initialState = {
|
||||
community: {},
|
||||
isFetchingPeople: false,
|
||||
errorPeople: '',
|
||||
accounts: [],
|
||||
users: [],
|
||||
searchValue: '',
|
||||
fieldPeople: 'created_at',
|
||||
ascPeople: false,
|
||||
totalPagesPeople: 0,
|
||||
@@ -27,19 +30,19 @@ const initialState = {
|
||||
|
||||
export default function community (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case FETCH_COMMENTERS_REQUEST :
|
||||
case FETCH_USERS_REQUEST :
|
||||
return {
|
||||
...state,
|
||||
isFetchingPeople: true,
|
||||
};
|
||||
case FETCH_COMMENTERS_FAILURE :
|
||||
case FETCH_USERS_FAILURE :
|
||||
return {
|
||||
...state,
|
||||
isFetchingPeople: false,
|
||||
errorPeople: action.error,
|
||||
};
|
||||
case FETCH_COMMENTERS_SUCCESS : {
|
||||
const {accounts, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line
|
||||
case FETCH_USERS_SUCCESS : {
|
||||
const {users, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line
|
||||
return {
|
||||
...state,
|
||||
isFetchingPeople: false,
|
||||
@@ -49,27 +52,32 @@ export default function community (state = initialState, action) {
|
||||
limitPeople: limit,
|
||||
totalPagesPeople: totalPages,
|
||||
...rest,
|
||||
accounts, // Sets to normal array
|
||||
users, // Sets to normal array
|
||||
};
|
||||
}
|
||||
case SET_PAGE:
|
||||
return {
|
||||
...state,
|
||||
pagePeople: action.page,
|
||||
};
|
||||
case SET_ROLE : {
|
||||
const commenters = state.accounts;
|
||||
const commenters = state.users;
|
||||
const idx = commenters.findIndex((el) => el.id === action.id);
|
||||
|
||||
commenters[idx].roles[0] = action.role;
|
||||
return {
|
||||
...state,
|
||||
accounts: commenters.map((id) => id),
|
||||
users: commenters.map((id) => id),
|
||||
};
|
||||
}
|
||||
case SET_COMMENTER_STATUS: {
|
||||
const commenters = state.accounts;
|
||||
const commenters = state.users;
|
||||
const idx = commenters.findIndex((el) => el.id === action.id);
|
||||
|
||||
commenters[idx].status = action.status;
|
||||
return {
|
||||
...state,
|
||||
accounts: commenters.map((id) => id),
|
||||
users: commenters.map((id) => id),
|
||||
};
|
||||
|
||||
}
|
||||
@@ -101,6 +109,11 @@ export default function community (state = initialState, action) {
|
||||
user: action.user,
|
||||
rejectUsernameDialog: true
|
||||
};
|
||||
case SET_SEARCH_VALUE:
|
||||
return {
|
||||
...state,
|
||||
searchValue: action.value,
|
||||
};
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import auth from './auth';
|
||||
import assets from './assets';
|
||||
import stories from './stories';
|
||||
import dashboard from './dashboard';
|
||||
import configure from './configure';
|
||||
import community from './community';
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
configure,
|
||||
suspendUserDialog,
|
||||
userDetail,
|
||||
assets,
|
||||
stories,
|
||||
community,
|
||||
moderation,
|
||||
install,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as actions from '../constants/stories';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
const initialState = {
|
||||
assets: {
|
||||
byId: {},
|
||||
ids: [],
|
||||
assets: []
|
||||
},
|
||||
searchValue: '',
|
||||
criteria: {
|
||||
asc: 'false',
|
||||
filter: 'all',
|
||||
},
|
||||
};
|
||||
|
||||
export default function assets (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FETCH_ASSETS_SUCCESS: {
|
||||
const assets = action.assets.reduce((prev, curr) => {
|
||||
prev[curr.id] = curr;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
return update(state, {
|
||||
assets: {
|
||||
totalPages: {$set: action.totalPages},
|
||||
page: {$set: action.page},
|
||||
byId: {$set: assets},
|
||||
count: {$set: action.count},
|
||||
ids: {$set: Object.keys(assets)},
|
||||
},
|
||||
});
|
||||
}
|
||||
case actions.UPDATE_ASSET_STATE_REQUEST:
|
||||
return update(state, {
|
||||
assets: {
|
||||
byId: {
|
||||
[action.id]: {
|
||||
closedAt: {$set: action.closedAt},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
case actions.UPDATE_ASSETS:
|
||||
return update(state, {
|
||||
assets: {
|
||||
assets: {$set: action.assets},
|
||||
},
|
||||
});
|
||||
case actions.SET_PAGE:
|
||||
return {
|
||||
...state,
|
||||
page: action.page,
|
||||
};
|
||||
case actions.SET_SEARCH_VALUE:
|
||||
return {
|
||||
...state,
|
||||
searchValue: action.value,
|
||||
};
|
||||
case actions.SET_CRITERIA:
|
||||
return {
|
||||
...state,
|
||||
criteria: {
|
||||
...state.criteria,
|
||||
...action.criteria,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -1,78 +1,18 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import CommunityMenu from './CommunityMenu';
|
||||
import People from './People';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Community.css';
|
||||
import People from '../containers/People';
|
||||
import CommunityMenu from './CommunityMenu';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
|
||||
export default class Community extends Component {
|
||||
|
||||
state = {
|
||||
searchValue: '',
|
||||
timer: null
|
||||
};
|
||||
|
||||
onKeyDownHandler = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
onSearchChange = (e) => {
|
||||
const value = e.target.value;
|
||||
this.setState((prevState) => {
|
||||
prevState.searchValue = value;
|
||||
clearTimeout(prevState.timer);
|
||||
const fetchAccounts = this.props.fetchAccounts;
|
||||
prevState.timer = setTimeout(() => {
|
||||
fetchAccounts({value});
|
||||
}, 350);
|
||||
return prevState;
|
||||
});
|
||||
}
|
||||
|
||||
onHeaderClickHandler = (sort) => {
|
||||
this.props.updateSorting(sort);
|
||||
this.search();
|
||||
}
|
||||
|
||||
onNewPageHandler = (page) => {
|
||||
this.props.newPage(page);
|
||||
this.search({page});
|
||||
}
|
||||
|
||||
search(query = {}) {
|
||||
const {community} = this.props;
|
||||
|
||||
this.props.fetchAccounts({
|
||||
value: this.state.searchValue,
|
||||
field: community.fieldPeople,
|
||||
asc: community.ascPeople,
|
||||
...query
|
||||
});
|
||||
}
|
||||
|
||||
getTabContent(searchValue, props) {
|
||||
const {community} = props;
|
||||
const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path;
|
||||
class Community extends Component {
|
||||
renderTab() {
|
||||
const {route, community, ...props} = this.props;
|
||||
const activeTab = route.path === ':id' ? 'flagged' : route.path;
|
||||
|
||||
if (activeTab === 'people') {
|
||||
return (
|
||||
<People
|
||||
isFetching={community.isFetchingPeople}
|
||||
commenters={community.accounts}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={this.onSearchChange}
|
||||
error={community.errorPeople}
|
||||
totalPages={community.totalPagesPeople}
|
||||
page={community.pagePeople}
|
||||
onKeyDown={this.onKeyDownHandler}
|
||||
onHeaderClickHandler={this.onHeaderClickHandler}
|
||||
onNewPageHandler={this.onNewPageHandler}
|
||||
/>
|
||||
);
|
||||
return <People community={community} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -82,37 +22,36 @@ export default class Community extends Component {
|
||||
root={this.props.root}
|
||||
/>
|
||||
<RejectUsernameDialog
|
||||
open={community.rejectUsernameDialog}
|
||||
handleClose={props.hideRejectUsernameDialog}
|
||||
user={community.user}
|
||||
open={community.rejectUsernameDialog}
|
||||
rejectUsername={props.rejectUsername}
|
||||
handleClose={props.hideRejectUsernameDialog}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {searchValue} = this.state;
|
||||
const tab = this.getTabContent(searchValue, this.props);
|
||||
const {root: {flaggedUsernamesCount}} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="talk-admin-community">
|
||||
<CommunityMenu flaggedUsernamesCount={flaggedUsernamesCount} />
|
||||
<div>{tab}</div>
|
||||
<div className={styles.container}>
|
||||
{this.renderTab()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Community.propTypes = {
|
||||
community: PropTypes.object,
|
||||
fetchAccounts: PropTypes.func,
|
||||
hideRejectUsernameDialog: PropTypes.func,
|
||||
updateSorting: PropTypes.func,
|
||||
newPage: PropTypes.func,
|
||||
route: PropTypes.object,
|
||||
rejectUsername: PropTypes.func,
|
||||
community: PropTypes.object,
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
hideRejectUsernameDialog: PropTypes.func.isRequired,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Community;
|
||||
|
||||
@@ -1,33 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './styles.css';
|
||||
import Table from '../containers/Table';
|
||||
import {Pager, Icon} from 'coral-ui';
|
||||
import Table from './Table';
|
||||
import {Icon} from 'coral-ui';
|
||||
import EmptyCard from '../../../components/EmptyCard';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const tableHeaders = [
|
||||
{
|
||||
title: t('community.username_and_email'),
|
||||
field: 'username'
|
||||
},
|
||||
{
|
||||
title: t('community.account_creation_date'),
|
||||
field: 'created_at'
|
||||
},
|
||||
{
|
||||
title: t('community.status'),
|
||||
field: 'status'
|
||||
},
|
||||
{
|
||||
title: t('community.newsroom_role'),
|
||||
field: 'role'
|
||||
}
|
||||
];
|
||||
const People = (props) => {
|
||||
const {
|
||||
users = [],
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onHeaderClickHandler,
|
||||
onPageChange,
|
||||
totalPages,
|
||||
page,
|
||||
setRole,
|
||||
setCommenterStatus,
|
||||
viewUserDetail,
|
||||
} = props;
|
||||
|
||||
const hasResults = !!users.length;
|
||||
|
||||
const People = ({commenters, searchValue, onSearchChange, ...props}) => {
|
||||
const hasResults = !!commenters.length;
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
@@ -47,28 +42,33 @@ const People = ({commenters, searchValue, onSearchChange, ...props}) => {
|
||||
{
|
||||
hasResults
|
||||
? <Table
|
||||
headers={tableHeaders}
|
||||
commenters={commenters}
|
||||
onHeaderClickHandler={props.onHeaderClickHandler}
|
||||
users={users}
|
||||
setRole={setRole}
|
||||
viewUserDetail={viewUserDetail}
|
||||
setCommenterStatus={setCommenterStatus}
|
||||
onHeaderClickHandler={onHeaderClickHandler}
|
||||
pageCount={totalPages}
|
||||
onPageChange={onPageChange}
|
||||
page={page}
|
||||
/>
|
||||
: <EmptyCard>{t('community.no_results')}</EmptyCard>
|
||||
}
|
||||
<Pager
|
||||
totalPages={props.totalPages}
|
||||
page={props.page}
|
||||
onNewPageHandler={props.onNewPageHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
People.propTypes = {
|
||||
commenters: PropTypes.array,
|
||||
onHeaderClickHandler: PropTypes.func,
|
||||
users: PropTypes.array,
|
||||
page: PropTypes.number.isRequired,
|
||||
searchValue: PropTypes.string,
|
||||
onSearchChange: PropTypes.func,
|
||||
totalPages: PropTypes.number,
|
||||
onNewPageHandler: PropTypes.func,
|
||||
onPageChange: PropTypes.func,
|
||||
setCommenterStatus: PropTypes.func.isRequired,
|
||||
setRole: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default People;
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
width: 100%;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
th.header {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
th:nth-child(2), th:nth-child(3) {
|
||||
width: 100px;
|
||||
}
|
||||
th.header:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
th.header:nth-child(2), th.header:nth-child(3) {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
@@ -1,68 +1,96 @@
|
||||
import React from 'react';
|
||||
import styles from '../components/Table.css';
|
||||
import styles from './Table.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dropdown, Option} from 'coral-ui';
|
||||
import {Paginate, Dropdown, Option} from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
|
||||
const Table = ({headers, commenters, onHeaderClickHandler, onRoleChange, onCommenterStatusChange, viewUserDetail}) => (
|
||||
<table className={`mdl-data-table ${styles.dataTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, i) =>(
|
||||
<th
|
||||
key={i}
|
||||
className="mdl-data-table__cell--non-numeric"
|
||||
scope="col"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{commenters.map((row, i)=> (
|
||||
<tr key={i}>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<button onClick={() => {viewUserDetail(row.id);}} className={cn(styles.username, styles.button)}>{row.username}</button>
|
||||
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.created_at}
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
value={row.status}
|
||||
placeholder={t('community.status')}
|
||||
onChange={(status) => onCommenterStatusChange(row.id, status)}>
|
||||
<Option value={'ACTIVE'} label={t('community.active')} />
|
||||
<Option value={'BANNED'} label={t('community.banned')} />
|
||||
</Dropdown>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
value={row.roles[0] || ''}
|
||||
placeholder={t('community.role')}
|
||||
onChange={(role) => onRoleChange(row.id, role)}>
|
||||
<Option value={''} label={t('community.none')} />
|
||||
<Option value={'STAFF'} label={t('community.staff')} />
|
||||
<Option value={'MODERATOR'} label={t('community.moderator')} />
|
||||
<Option value={'ADMIN'} label={t('community.admin')} />
|
||||
</Dropdown>
|
||||
</td>
|
||||
const headers = [
|
||||
{
|
||||
title: t('community.username_and_email'),
|
||||
field: 'username'
|
||||
},
|
||||
{
|
||||
title: t('community.account_creation_date'),
|
||||
field: 'created_at'
|
||||
},
|
||||
{
|
||||
title: t('community.status'),
|
||||
field: 'status'
|
||||
},
|
||||
{
|
||||
title: t('community.newsroom_role'),
|
||||
field: 'role'
|
||||
}
|
||||
];
|
||||
|
||||
const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUserDetail, pageCount, page, onPageChange}) => (
|
||||
<div>
|
||||
<table className={`mdl-data-table ${styles.dataTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, i) =>(
|
||||
<th
|
||||
key={i}
|
||||
className={cn('mdl-data-table__cell--non-numeric', styles.header)}
|
||||
scope="col"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((row, i)=> (
|
||||
<tr key={i}>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<button onClick={() => {viewUserDetail(row.id);}} className={cn(styles.username, styles.button)}>{row.username}</button>
|
||||
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.created_at}
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
value={row.status}
|
||||
placeholder={t('community.status')}
|
||||
onChange={(status) => setCommenterStatus(row.id, status)}>
|
||||
<Option value={'ACTIVE'} label={t('community.active')} />
|
||||
<Option value={'BANNED'} label={t('community.banned')} />
|
||||
</Dropdown>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
value={row.roles[0] || ''}
|
||||
placeholder={t('community.role')}
|
||||
onChange={(role) => setRole(row.id, role)}>
|
||||
<Option value={''} label={t('community.none')} />
|
||||
<Option value={'STAFF'} label={t('community.staff')} />
|
||||
<Option value={'MODERATOR'} label={t('community.moderator')} />
|
||||
<Option value={'ADMIN'} label={t('community.admin')} />
|
||||
</Dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Paginate
|
||||
pageCount={pageCount}
|
||||
page={page - 1}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Table.propTypes = {
|
||||
headers: PropTypes.array,
|
||||
commenters: PropTypes.array,
|
||||
onHeaderClickHandler: PropTypes.func,
|
||||
onRoleChange: PropTypes.func,
|
||||
onCommenterStatusChange: PropTypes.func,
|
||||
viewUserDetail: PropTypes.func,
|
||||
users: PropTypes.array,
|
||||
onHeaderClickHandler: PropTypes.func.isRequired,
|
||||
setRole: PropTypes.func.isRequired,
|
||||
setCommenterStatus: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
pageCount: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Table;
|
||||
|
||||
@@ -1,65 +1,21 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
|
||||
import {
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
newPage,
|
||||
hideRejectUsernameDialog
|
||||
} from '../../../actions/community';
|
||||
|
||||
import {hideRejectUsernameDialog} from '../../../actions/community';
|
||||
import Community from '../components/Community';
|
||||
|
||||
class CommunityContainer extends Component {
|
||||
|
||||
componentWillMount() {
|
||||
this.props.fetchAccounts({});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Community
|
||||
fetchAccounts={this.props.fetchAccounts}
|
||||
community={this.props.community}
|
||||
hideRejectUsernameDialog={this.props.hideRejectUsernameDialog}
|
||||
updateSorting={this.props.updateSorting}
|
||||
newPage={this.props.newPage}
|
||||
route={this.props.route}
|
||||
rejectUsername={this.props.rejectUsername}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
const mapStateToProps = (state) => ({
|
||||
community: state.community,
|
||||
});
|
||||
|
||||
CommunityContainer.propTypes = {
|
||||
community: PropTypes.object,
|
||||
fetchAccounts: PropTypes.func,
|
||||
hideRejectUsernameDialog: PropTypes.func,
|
||||
updateSorting: PropTypes.func,
|
||||
newPage: PropTypes.func,
|
||||
route: PropTypes.object,
|
||||
rejectUsername: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
fetchAccounts,
|
||||
hideRejectUsernameDialog,
|
||||
updateSorting,
|
||||
newPage,
|
||||
}, dispatch);
|
||||
|
||||
const withData = withQuery(gql`
|
||||
@@ -89,4 +45,4 @@ export default compose(
|
||||
withSetUserStatus,
|
||||
withRejectUsername,
|
||||
withData
|
||||
)(CommunityContainer);
|
||||
)(Community);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import People from '../components/People';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {viewUserDetail} from '../../../actions/userDetail';
|
||||
|
||||
import {
|
||||
fetchUsers,
|
||||
updateSorting,
|
||||
setPage,
|
||||
hideRejectUsernameDialog,
|
||||
setCommenterStatus,
|
||||
setRole,
|
||||
setSearchValue,
|
||||
} from '../../../actions/community';
|
||||
|
||||
class PeopleContainer extends React.Component {
|
||||
timer=null;
|
||||
|
||||
fetchUsers = (query = {}) => {
|
||||
const {community} = this.props;
|
||||
|
||||
this.props.fetchUsers({
|
||||
value: community.searchValue,
|
||||
field: community.fieldPeople,
|
||||
asc: community.ascPeople,
|
||||
...query
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.fetchUsers();
|
||||
}
|
||||
|
||||
onKeyDownHandler = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.fetchUsers();
|
||||
}
|
||||
}
|
||||
|
||||
onSearchChange = (e) => {
|
||||
const value = e.target.value;
|
||||
|
||||
this.props.setSearchValue(value);
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
this.fetchUsers({value});
|
||||
}, 350);
|
||||
}
|
||||
|
||||
onHeaderClickHandler = (sort) => {
|
||||
this.props.updateSorting(sort);
|
||||
this.fetchUsers();
|
||||
}
|
||||
|
||||
onPageChange = ({selected}) => {
|
||||
const page = selected + 1;
|
||||
this.props.setPage(page);
|
||||
this.fetchUsers({page});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <People
|
||||
users={this.props.community.users}
|
||||
searchValue={this.props.community.searchValue}
|
||||
onSearchChange={this.onSearchChange}
|
||||
onHeaderClickHandler={this.onHeaderClickHandler}
|
||||
onPageChange={this.onPageChange}
|
||||
totalPages={this.props.community.totalPagesPeople}
|
||||
setCommenterStatus={this.props.setCommenterStatus}
|
||||
setRole={this.props.setRole}
|
||||
page={this.props.community.pagePeople}
|
||||
viewUserDetail={this.props.viewUserDetail}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
PeopleContainer.propTypes = {
|
||||
setPage: PropTypes.func,
|
||||
fetchUsers: PropTypes.func,
|
||||
updateSorting: PropTypes.func,
|
||||
setRole: PropTypes.func.isRequired,
|
||||
setCommenterStatus: PropTypes.func.isRequired,
|
||||
setSearchValue: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
community: PropTypes.object,
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
setPage,
|
||||
fetchUsers,
|
||||
updateSorting,
|
||||
hideRejectUsernameDialog,
|
||||
setCommenterStatus,
|
||||
setRole,
|
||||
viewUserDetail,
|
||||
setSearchValue,
|
||||
}, dispatch);
|
||||
|
||||
export default connect(null, mapDispatchToProps)(PeopleContainer);
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {setRole, setCommenterStatus} from '../../../actions/community';
|
||||
import Table from '../components/Table';
|
||||
import {viewUserDetail} from '../../../actions/userDetail';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class TableContainer extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Table
|
||||
{...this.props}
|
||||
onRoleChange={this.props.setRole}
|
||||
onCommenterStatusChange={this.props.setCommenterStatus}
|
||||
commenters={this.props.commenters}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
TableContainer.propTypes = {
|
||||
setRole: PropTypes.func,
|
||||
setCommenterStatus: PropTypes.func,
|
||||
commenters: PropTypes.array,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
commenters: state.community.accounts,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
setCommenterStatus,
|
||||
setRole,
|
||||
viewUserDetail,
|
||||
}, dispatch);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TableContainer);
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dropdow {
|
||||
.dropdownContainer {
|
||||
position: relative;
|
||||
margin-top: 5px;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class ViewOptions extends React.Component {
|
||||
<li className={styles.viewOptionsItem}>
|
||||
Sort Comments
|
||||
<Dropdown
|
||||
containerClassName={styles.dropdownContainer}
|
||||
toggleClassName={styles.dropdownToggle}
|
||||
toggleOpenClassName={styles.dropdownToggleOpen}
|
||||
placeholder={t('modqueue.sort')}
|
||||
|
||||
@@ -1,65 +1,20 @@
|
||||
import React, {Component} from 'react';
|
||||
import cn from 'classnames';
|
||||
import {Link} from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {Dropdown, Option, Pager, Icon} from 'coral-ui';
|
||||
import {Dropdown, Option, Paginate, Icon} from 'coral-ui';
|
||||
import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './Stories.css';
|
||||
import EmptyCard from 'coral-admin/src/components/EmptyCard';
|
||||
|
||||
const limit = 25;
|
||||
|
||||
class Stories extends Component {
|
||||
|
||||
state = {
|
||||
search: '',
|
||||
sort: 'desc',
|
||||
filter: 'all',
|
||||
statusMenus: {},
|
||||
timer: null,
|
||||
page: 0
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchAssets(0, limit, '', this.state.sortBy);
|
||||
}
|
||||
|
||||
onSettingChange = (setting) => (e) => {
|
||||
let options = this.state;
|
||||
this.setState({[setting]: e.target.value});
|
||||
options[setting] = e.target.value;
|
||||
this.props.fetchAssets(0, limit, options.search, options.sort, options.filter);
|
||||
}
|
||||
|
||||
onSearchChange = (e) => {
|
||||
const search = e.target.value;
|
||||
this.setState((prevState) => {
|
||||
prevState.search = search;
|
||||
clearTimeout(prevState.timer);
|
||||
const fetchAssets = this.props.fetchAssets;
|
||||
prevState.timer = setTimeout(() => {
|
||||
fetchAssets(0, limit, search, this.state.sort, this.state.filter);
|
||||
}, 350);
|
||||
return prevState;
|
||||
});
|
||||
}
|
||||
|
||||
renderDate = (date) => {
|
||||
const d = new Date(date);
|
||||
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
onStatusChange = async (closeStream, id) => {
|
||||
try {
|
||||
this.props.updateAssetState(id, closeStream ? Date.now() : null);
|
||||
const {search, sort, filter, page} = this.state;
|
||||
this.props.fetchAssets(page, limit, search, sort, filter);
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
renderTitle = (title, {id}) => <Link to={`/admin/moderate/${id}`}>{title}</Link>
|
||||
|
||||
renderStatus = (closedAt, {id}) => {
|
||||
@@ -67,39 +22,27 @@ class Stories extends Component {
|
||||
return (
|
||||
<Dropdown
|
||||
value={closed}
|
||||
onChange={(value) => this.onStatusChange(value, id)}>
|
||||
onChange={(value) => this.props.onStatusChange(value, id)}>
|
||||
<Option value={false} label={t('streams.open')} />
|
||||
<Option value={true} label={t('streams.closed')} />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
onPageClick = (page) => {
|
||||
this.setState({page});
|
||||
const {search, sort, filter} = this.state;
|
||||
this.props.fetchAssets((page - 1) * limit, limit, search, sort, filter);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {search, sort, filter} = this.state;
|
||||
const {assets} = this.props;
|
||||
|
||||
const assetsIds = sortBy(assets.ids.map((id) => assets.byId[id]), 'publication_date');
|
||||
|
||||
if (this.state.sort === 'desc') {
|
||||
assetsIds.reverse();
|
||||
}
|
||||
const {assets, searchValue, filter, onSearchChange, onSettingChange, onPageChange, asc} = this.props;
|
||||
const rows = assets.ids.map((id) => assets.byId[id]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={cn('talk-admin-stories', styles.container)}>
|
||||
<div className={styles.leftColumn}>
|
||||
<div className={styles.searchBox}>
|
||||
<Icon name='search' className={styles.searchIcon}/>
|
||||
<input
|
||||
type='text'
|
||||
value={search}
|
||||
value={searchValue}
|
||||
className={styles.searchBoxInput}
|
||||
onChange={this.onSearchChange}
|
||||
onChange={onSearchChange}
|
||||
placeholder={t('streams.search')}/>
|
||||
</div>
|
||||
<div className={styles.optionHeader}>{t('streams.filter_streams')}</div>
|
||||
@@ -108,7 +51,7 @@ class Stories extends Component {
|
||||
name='status filter'
|
||||
value={filter}
|
||||
childContainer='div'
|
||||
onChange={this.onSettingChange('filter')}
|
||||
onChange={onSettingChange('filter')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value='all'>{t('streams.all')}</Radio>
|
||||
@@ -118,19 +61,19 @@ class Stories extends Component {
|
||||
<div className={styles.optionHeader}>{t('streams.sort_by')}</div>
|
||||
<RadioGroup
|
||||
name='sort by'
|
||||
value={sort}
|
||||
value={asc}
|
||||
childContainer='div'
|
||||
onChange={this.onSettingChange('sort')}
|
||||
onChange={onSettingChange('asc')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value='desc'>{t('streams.newest')}</Radio>
|
||||
<Radio value='asc'>{t('streams.oldest')}</Radio>
|
||||
<Radio value='false'>{t('streams.newest')}</Radio>
|
||||
<Radio value='true'>{t('streams.oldest')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{
|
||||
assetsIds.length
|
||||
rows.length
|
||||
? <div className={styles.mainContent}>
|
||||
<DataTable className={styles.streamsTable} rows={assetsIds} onClick={this.goToModeration}>
|
||||
<DataTable className={styles.streamsTable} rows={rows}>
|
||||
<TableHeader name="title" cellFormatter={this.renderTitle}>{t('streams.article')}</TableHeader>
|
||||
<TableHeader name="publication_date" cellFormatter={this.renderDate}>
|
||||
{t('streams.pubdate')}
|
||||
@@ -139,10 +82,10 @@ class Stories extends Component {
|
||||
{t('streams.status')}
|
||||
</TableHeader>
|
||||
</DataTable>
|
||||
<Pager
|
||||
totalPages={Math.ceil((assets.count || 0) / limit)}
|
||||
page={this.state.page}
|
||||
onNewPageHandler={this.onPageClick} />
|
||||
<Paginate
|
||||
pageCount={assets.totalPages}
|
||||
page={assets.page - 1}
|
||||
onPageChange={onPageChange} />
|
||||
</div>
|
||||
: <EmptyCard>{t('streams.empty_result')}</EmptyCard>
|
||||
}
|
||||
@@ -153,8 +96,13 @@ class Stories extends Component {
|
||||
|
||||
Stories.propTypes = {
|
||||
assets: PropTypes.object,
|
||||
fetchAssets: PropTypes.func,
|
||||
updateAssetState: PropTypes.func,
|
||||
searchValue: PropTypes.string,
|
||||
asc: PropTypes.string,
|
||||
filter: PropTypes.string,
|
||||
onStatusChange: PropTypes.func.isRequired,
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
onSettingChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Stories;
|
||||
|
||||
@@ -1,27 +1,106 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose} from 'react-apollo';
|
||||
import {fetchAssets, updateAssetState} from 'coral-admin/src/actions/assets';
|
||||
import {fetchAssets, updateAssetState, setPage, setSearchValue, setCriteria} from 'coral-admin/src/actions/stories';
|
||||
import Stories from '../components/Stories';
|
||||
|
||||
class StoriesContainer extends Component {
|
||||
timer=null;
|
||||
|
||||
componentDidMount () {
|
||||
this.fetchAssets();
|
||||
}
|
||||
|
||||
onSearchChange = (e) => {
|
||||
const {value} = e.target;
|
||||
|
||||
this.props.setSearchValue(value);
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
this.fetchAssets();
|
||||
}, 350);
|
||||
}
|
||||
|
||||
onSettingChange = (setting) => (e) => {
|
||||
const criteria = {[setting]: e.target.value};
|
||||
this.props.setCriteria(criteria);
|
||||
this.fetchAssets(criteria);
|
||||
}
|
||||
|
||||
fetchAssets = (query) => {
|
||||
const {searchValue, asc, filter, limit} = this.props;
|
||||
|
||||
this.props.fetchAssets({
|
||||
value: searchValue,
|
||||
asc,
|
||||
filter,
|
||||
limit,
|
||||
...query
|
||||
});
|
||||
};
|
||||
|
||||
onStatusChange = async (closeStream, id) => {
|
||||
const {updateAssetState} = this.props;
|
||||
|
||||
try {
|
||||
updateAssetState(id, closeStream ? Date.now() : null);
|
||||
this.fetchAssets();
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
onPageChange = ({selected}) => {
|
||||
const page = selected + 1;
|
||||
this.props.setPage(page);
|
||||
this.fetchAssets({page});
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Stories {...this.props} />;
|
||||
return <Stories
|
||||
assets={this.props.assets}
|
||||
searchValue={this.props.searchValue}
|
||||
asc={this.props.asc}
|
||||
filter={this.props.filter}
|
||||
limit={this.props.limit}
|
||||
onPageChange={this.onPageChange}
|
||||
onStatusChange={this.onStatusChange}
|
||||
onSettingChange={this.onSettingChange}
|
||||
onSearchChange={this.onSearchChange}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
assets: state.assets
|
||||
const mapStateToProps = ({stories}) => ({
|
||||
assets: stories.assets,
|
||||
searchValue: stories.searchValue,
|
||||
asc: stories.criteria.asc,
|
||||
filter: stories.criteria.filter,
|
||||
limit: stories.criteria.limit,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
setPage,
|
||||
setCriteria,
|
||||
setSearchValue,
|
||||
fetchAssets,
|
||||
updateAssetState,
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
)(StoriesContainer);
|
||||
StoriesContainer.propTypes = {
|
||||
assets: PropTypes.object,
|
||||
searchValue: PropTypes.string,
|
||||
asc: PropTypes.string,
|
||||
filter: PropTypes.string,
|
||||
limit: PropTypes.number,
|
||||
setPage: PropTypes.func.isRequired,
|
||||
setCriteria: PropTypes.func.isRequired,
|
||||
setSearchValue: PropTypes.func.isRequired,
|
||||
fetchAssets: PropTypes.func.isRequired,
|
||||
updateAssetState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StoriesContainer);
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ export const hideSignInDialog = () => (dispatch) => {
|
||||
dispatch({type: actions.HIDE_SIGNIN_DIALOG});
|
||||
};
|
||||
|
||||
export const resetSignInDialog = () => (dispatch) => {
|
||||
dispatch({type: actions.HIDE_SIGNIN_DIALOG});
|
||||
};
|
||||
|
||||
export const focusSignInDialog = () => ({
|
||||
type: actions.FOCUS_SIGNIN_DIALOG,
|
||||
});
|
||||
@@ -94,8 +98,9 @@ export const cleanState = () => ({
|
||||
|
||||
// Sign In Actions
|
||||
|
||||
const signInRequest = () => ({
|
||||
type: actions.FETCH_SIGNIN_REQUEST
|
||||
const signInRequest = (email) => ({
|
||||
type: actions.FETCH_SIGNIN_REQUEST,
|
||||
email,
|
||||
});
|
||||
|
||||
const signInFailure = (error) => ({
|
||||
@@ -122,7 +127,7 @@ export const handleAuthToken = (token) => (dispatch, _, {storage}) => {
|
||||
|
||||
export const fetchSignIn = (formData) => {
|
||||
return (dispatch, _, {rest}) => {
|
||||
dispatch(signInRequest());
|
||||
dispatch(signInRequest(formData.email));
|
||||
|
||||
return rest('/auth/local', {method: 'POST', body: formData})
|
||||
.then(({token}) => {
|
||||
@@ -144,8 +149,7 @@ export const fetchSignIn = (formData) => {
|
||||
// invalid credentials
|
||||
dispatch(signInFailure(t('error.email_password'), error.metadata));
|
||||
} else {
|
||||
const str = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(signInFailure(str));
|
||||
dispatch(signInFailure(error));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -349,8 +353,9 @@ const verifyEmailSuccess = () => ({
|
||||
type: actions.VERIFY_EMAIL_SUCCESS
|
||||
});
|
||||
|
||||
const verifyEmailFailure = () => ({
|
||||
type: actions.VERIFY_EMAIL_FAILURE
|
||||
const verifyEmailFailure = (error) => ({
|
||||
type: actions.VERIFY_EMAIL_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestConfirmEmail = (email) => (dispatch, getState, {rest}) => {
|
||||
@@ -366,8 +371,8 @@ export const requestConfirmEmail = (email) => (dispatch, getState, {rest}) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(verifyEmailFailure(errorMessage));
|
||||
dispatch(verifyEmailFailure(error));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -53,3 +53,4 @@ export const UPDATE_USERNAME = 'UPDATE_USERNAME';
|
||||
export const SET_REQUIRE_EMAIL_VERIFICATION = 'SET_REQUIRE_EMAIL_VERIFICATION';
|
||||
export const SET_REDIRECT_URI = 'SET_REDIRECT_URI';
|
||||
|
||||
export const RESET_SIGNIN_DIALOG = 'RESET_SIGNIN_DIALOG';
|
||||
|
||||
@@ -10,7 +10,7 @@ const initialState = {
|
||||
showCreateUsernameDialog: false,
|
||||
checkedInitialLogin: false,
|
||||
view: 'SIGNIN',
|
||||
error: '',
|
||||
error: null,
|
||||
passwordRequestSuccess: null,
|
||||
passwordRequestFailure: null,
|
||||
emailVerificationFailure: false,
|
||||
@@ -45,14 +45,15 @@ export default function auth (state = initialState, action) {
|
||||
showSignInDialog: true,
|
||||
signInDialogFocus: true,
|
||||
};
|
||||
case actions.HIDE_SIGNIN_DIALOG :
|
||||
case actions.RESET_SIGNIN_DIALOG:
|
||||
case actions.HIDE_SIGNIN_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
showSignInDialog: false,
|
||||
signInDialogFocus: false,
|
||||
view: 'SIGNIN',
|
||||
error: '',
|
||||
error: null,
|
||||
passwordRequestFailure: null,
|
||||
passwordRequestSuccess: null,
|
||||
emailVerificationFailure: false,
|
||||
@@ -74,7 +75,7 @@ export default function auth (state = initialState, action) {
|
||||
return {
|
||||
...state,
|
||||
showCreateUsernameDialog: false,
|
||||
error: '',
|
||||
error: null,
|
||||
};
|
||||
case actions.CREATE_USERNAME_FAILURE:
|
||||
return {
|
||||
@@ -92,6 +93,7 @@ export default function auth (state = initialState, action) {
|
||||
case actions.FETCH_SIGNIN_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
email: action.email,
|
||||
isLoading: true,
|
||||
};
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
@@ -120,6 +122,7 @@ export default function auth (state = initialState, action) {
|
||||
isLoading: false,
|
||||
error: action.error,
|
||||
user: null,
|
||||
view: action.error.translation_key === 'EMAIL_NOT_VERIFIED' ? 'RESEND_VERIFICATION' : state.view,
|
||||
};
|
||||
case actions.FETCH_SIGNUP_FACEBOOK_REQUEST:
|
||||
return {
|
||||
@@ -175,7 +178,7 @@ export default function auth (state = initialState, action) {
|
||||
case actions.VALID_FORM:
|
||||
return {
|
||||
...state,
|
||||
error: '',
|
||||
error: null,
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
|
||||
return {
|
||||
@@ -200,7 +203,7 @@ export default function auth (state = initialState, action) {
|
||||
case actions.VERIFY_EMAIL_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
emailVerificationFailure: true,
|
||||
emailVerificationFailure: action.error,
|
||||
emailVerificationLoading: false,
|
||||
};
|
||||
case actions.VERIFY_EMAIL_REQUEST:
|
||||
|
||||
@@ -22,6 +22,9 @@ export default class Popup extends Component {
|
||||
);
|
||||
|
||||
this.setCallbacks();
|
||||
|
||||
// For some reasons IE needs a timeout before setting the callbacks...
|
||||
setTimeout(() => this.setCallbacks(), 1000);
|
||||
}
|
||||
|
||||
setCallbacks() {
|
||||
|
||||
@@ -128,10 +128,10 @@ class Dropdown extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {className, toggleClassName, toggleOpenClassName} = this.props;
|
||||
const {containerClassName, toggleClassName, toggleOpenClassName} = this.props;
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.hideMenu}>
|
||||
<div className={cn(styles.dropdown, className)}>
|
||||
<div className={cn(styles.dropdown, containerClassName)}>
|
||||
<div
|
||||
className={cn(styles.toggle, toggleClassName, {[cn(this.state.isOpen, toggleOpenClassName)]: this.state.isOpen})}
|
||||
onClick={this.handleClick}
|
||||
@@ -170,7 +170,7 @@ class Dropdown extends React.Component {
|
||||
}
|
||||
|
||||
Dropdown.propTypes = {
|
||||
className: PropTypes.string,
|
||||
containerClassName: PropTypes.string,
|
||||
toggleClassName: PropTypes.string,
|
||||
toggleOpenClassName: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
.pager {
|
||||
text-align: center;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
color: white;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
line-height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.current {
|
||||
background: #696969;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Pager.css';
|
||||
|
||||
const Rows = (curr, total, onClickHandler) => Array.from(Array(total)).map((e, i) =>
|
||||
<li className={curr === i ? styles.current : ''}
|
||||
key={i} onClick={() => onClickHandler(i + 1)}>
|
||||
{i + 1}
|
||||
</li>
|
||||
);
|
||||
|
||||
const Pager = ({totalPages, page, onNewPageHandler}) => (
|
||||
<div className={styles.pager}>
|
||||
<ul>
|
||||
{
|
||||
(totalPages > page && totalPages > 1) ?
|
||||
<li
|
||||
onClick={() => onNewPageHandler(page - 1)}>
|
||||
Prev
|
||||
</li>
|
||||
:
|
||||
null
|
||||
}
|
||||
{Rows(page, totalPages, onNewPageHandler)}
|
||||
{
|
||||
(page < totalPages && totalPages > 1) ?
|
||||
<li
|
||||
onClick={() => onNewPageHandler(page + 1)}>
|
||||
Next
|
||||
</li>
|
||||
:
|
||||
null
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
Pager.propTypes = {
|
||||
totalPages: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default Pager;
|
||||
@@ -0,0 +1,33 @@
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page, .previous, .next, .break {
|
||||
display: inline-block;
|
||||
list-style: none;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.pageLink, .previousLink, .nextLink {
|
||||
display: inline-block;
|
||||
color: #696969;
|
||||
cursor: pointer;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
line-height: 30px;
|
||||
width: 30px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.previousLink, .nextLink {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: #696969;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactPaginate from 'react-paginate';
|
||||
import styles from './Paginate.css';
|
||||
import Icon from './Icon';
|
||||
|
||||
const Paginate = ({pageCount, page, onPageChange}) => (
|
||||
<ReactPaginate
|
||||
initialPage={0}
|
||||
forcePage={page}
|
||||
pageCount={pageCount}
|
||||
pageRangeDisplayed={5}
|
||||
marginPagesDisplayed={2}
|
||||
onPageChange={onPageChange}
|
||||
breakClassName={styles.break}
|
||||
containerClassName={styles.container}
|
||||
pageClassName={styles.page}
|
||||
pageLinkClassName={styles.pageLink}
|
||||
activeClassName={styles.active}
|
||||
previousLabel={<Icon name="chevron_left"/>}
|
||||
previousClassName={styles.previous}
|
||||
previousLinkClassName={styles.previousLink}
|
||||
nextLabel={<Icon name="chevron_right"/>}
|
||||
nextClassName={styles.next}
|
||||
nextLinkClassName={styles.nextLink}
|
||||
/>
|
||||
);
|
||||
|
||||
Paginate.propTypes = {
|
||||
page: PropTypes.number.isRequired,
|
||||
pageCount: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Paginate;
|
||||
@@ -4,49 +4,28 @@
|
||||
}
|
||||
|
||||
.spinner {
|
||||
-webkit-animation: rotator 1.4s linear infinite;
|
||||
animation: rotator 1.4s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotator {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotator {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.path {
|
||||
stroke: #f67150;
|
||||
stroke-dasharray: 187;
|
||||
stroke-dashoffset: 0;
|
||||
-webkit-transform-origin: center;
|
||||
transform-origin: center;
|
||||
-webkit-animation: dash 1.4s ease-in-out infinite, colors 5.6s ease-in-out infinite;
|
||||
animation: dash 1.4s ease-in-out infinite, colors 5.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes colors {
|
||||
0% {
|
||||
stroke: #f67150;
|
||||
}
|
||||
100% {
|
||||
stroke: #f6a47e;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes colors {
|
||||
0% {
|
||||
stroke: #f67150;
|
||||
@@ -55,33 +34,35 @@
|
||||
stroke: #f6a47e;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: 187;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 46.75;
|
||||
-webkit-transform: rotate(135deg);
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 187;
|
||||
-webkit-transform: rotate(450deg);
|
||||
transform: rotate(450deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: 187;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 46.75;
|
||||
-webkit-transform: rotate(135deg);
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 187;
|
||||
-webkit-transform: rotate(450deg);
|
||||
transform: rotate(450deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fullRotator {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hack for IE and Edge as they don't support css animations on SVG elements. */
|
||||
_:-ms-lang(x), .path {
|
||||
stroke-dasharray: 160;
|
||||
}
|
||||
|
||||
_:-ms-lang(x), .spinner {
|
||||
animation: fullRotator 1.4s linear infinite;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export {default as Item} from './components/Item';
|
||||
export {default as Card} from './components/Card';
|
||||
export {default as TextField} from './components/TextField';
|
||||
export {default as Success} from './components/Success';
|
||||
export {default as Pager} from './components/Pager';
|
||||
export {default as Paginate} from './components/Paginate';
|
||||
export {default as Wizard} from './components/Wizard';
|
||||
export {default as WizardNav} from './components/WizardNav';
|
||||
export {default as SnackBar} from './components/SnackBar';
|
||||
|
||||
@@ -163,7 +163,7 @@ const CONFIG = {
|
||||
SMTP_FROM_ADDRESS: process.env.TALK_SMTP_FROM_ADDRESS,
|
||||
SMTP_HOST: process.env.TALK_SMTP_HOST,
|
||||
SMTP_PASSWORD: process.env.TALK_SMTP_PASSWORD,
|
||||
SMTP_PORT: process.env.TALK_SMTP_PORT,
|
||||
SMTP_PORT: process.env.TALK_SMTP_PORT ? parseInt(process.env.TALK_SMTP_PORT) : undefined,
|
||||
SMTP_USERNAME: process.env.TALK_SMTP_USERNAME,
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -7,9 +7,9 @@ permalink: /commenter-features/
|
||||
|
||||
There are 2 ways that newsrooms can support signup/login functionality with Talk:
|
||||
|
||||
*Use Talk’s auth plugin out of the box (supports account registration with username and password, as well as features like forgot password)
|
||||
* Use Talk’s auth plugin out of the box (supports account registration with username and password, as well as features like forgot password)
|
||||
|
||||
*Create their own auth plugin to integrate with your own auth systems
|
||||
* Create their own auth plugin to integrate with your own auth systems
|
||||
|
||||
We also provide a Facebook auth plugin that supports logging in with Facebook (you must provide your own Facebook App ID and Secret, which you can read more about here: [https://developers.facebook.com](https://developers.facebook.com){:target="_blank"})
|
||||
|
||||
|
||||
@@ -97,7 +97,8 @@ class ErrAssetCommentingClosed extends APIError {
|
||||
class ErrAuthentication extends APIError {
|
||||
constructor(message = null) {
|
||||
super('authentication error occured', {
|
||||
status: 401
|
||||
status: 401,
|
||||
translation_key: 'AUTHENTICATION'
|
||||
}, {
|
||||
message
|
||||
});
|
||||
@@ -195,38 +196,52 @@ const ErrAssetURLAlreadyExists = new APIError('Asset URL already exists, cannot
|
||||
status: 409
|
||||
});
|
||||
|
||||
// ErrNotVerified is returned when a user tries to login with valid credentials
|
||||
// but their email address is not yet verified.
|
||||
const ErrNotVerified = new APIError('User does not have a verified email address', {
|
||||
translation_key: 'EMAIL_NOT_VERIFIED',
|
||||
status: 401,
|
||||
});
|
||||
|
||||
const ErrMaxRateLimit = new APIError('Rate limit exeeded', {
|
||||
translation_key: 'RATE_LIMIT_EXCEEDED',
|
||||
status: 429,
|
||||
});
|
||||
|
||||
// ErrCannotIgnoreStaff is returned when a user tries to ignore a staff member.
|
||||
const ErrCannotIgnoreStaff = new APIError('Cannot ignore staff members.', {
|
||||
translation_key: 'CANNOT_IGNORE_STAFF',
|
||||
status: 400
|
||||
status: 400,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
ExtendableError,
|
||||
APIError,
|
||||
ErrAlreadyExists,
|
||||
ErrPasswordTooShort,
|
||||
ErrSettingsNotInit,
|
||||
ErrAssetCommentingClosed,
|
||||
ErrAssetURLAlreadyExists,
|
||||
ErrAuthentication,
|
||||
ErrCannotIgnoreStaff,
|
||||
ErrCommentTooShort,
|
||||
ErrContainsProfanity,
|
||||
ErrEditWindowHasEnded,
|
||||
ErrEmailTaken,
|
||||
ErrInstallLock,
|
||||
ErrInvalidAssetURL,
|
||||
ErrLoginAttemptMaximumExceeded,
|
||||
ErrMaxRateLimit,
|
||||
ErrMissingEmail,
|
||||
ErrMissingPassword,
|
||||
ErrMissingToken,
|
||||
ErrEmailTaken,
|
||||
ErrSpecialChars,
|
||||
ErrMissingUsername,
|
||||
ErrContainsProfanity,
|
||||
ErrUsernameTaken,
|
||||
ErrAssetCommentingClosed,
|
||||
ErrNotFound,
|
||||
ErrInvalidAssetURL,
|
||||
ErrAuthentication,
|
||||
ErrNotAuthorized,
|
||||
ErrNotFound,
|
||||
ErrNotVerified,
|
||||
ErrPasswordTooShort,
|
||||
ErrPermissionUpdateUsername,
|
||||
ErrSameUsernameProvided,
|
||||
ErrSettingsInit,
|
||||
ErrInstallLock,
|
||||
ErrLoginAttemptMaximumExceeded,
|
||||
ErrEditWindowHasEnded,
|
||||
ErrCommentTooShort,
|
||||
ErrAssetURLAlreadyExists,
|
||||
ErrCannotIgnoreStaff
|
||||
};
|
||||
ErrSettingsNotInit,
|
||||
ErrSpecialChars,
|
||||
ErrUsernameTaken,
|
||||
ExtendableError,
|
||||
};
|
||||
@@ -194,8 +194,10 @@ en:
|
||||
NO_SPECIAL_CHARACTERS: "Usernames can contain letters numbers and _ only"
|
||||
PASSWORD_LENGTH: "Password is too short"
|
||||
PROFANITY_ERROR: "Usernames must not contain profanity. Please contact the administrator if you believe this to be in error."
|
||||
RATE_LIMIT_EXCEEDED: "Rate limit exceeded"
|
||||
USERNAME_IN_USE: "Username already in use"
|
||||
USERNAME_REQUIRED: "Must input a username"
|
||||
EMAIL_NOT_VERIFIED: "E-mail address not verified"
|
||||
EDIT_WINDOW_ENDED: "You can no longer edit this comment. The time window to do so has expired."
|
||||
EDIT_USERNAME_NOT_AUTHORIZED: "You do not have permission to update your username."
|
||||
SAME_USERNAME_PROVIDED: "You must submit a different username."
|
||||
|
||||
@@ -31,7 +31,7 @@ const nightwatch_config = {
|
||||
chrome: {
|
||||
desiredCapabilities: {
|
||||
browser: 'chrome',
|
||||
browser_version: '60',
|
||||
browser_version: '62',
|
||||
},
|
||||
},
|
||||
firefox: {
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ module.exports = {
|
||||
'chrome-headless': {
|
||||
desiredCapabilities: {
|
||||
chromeOptions : {
|
||||
args: ['--headless', '--disable-gpu'],
|
||||
args: ['--headless', '--disable-gpu', 'window-size=1280,800'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
"react-input-autosize": "^1.1.4",
|
||||
"react-mdl": "^1.7.2",
|
||||
"react-mdl-selectfield": "^0.2.0",
|
||||
"react-paginate": "^5.0.0",
|
||||
"react-recaptcha": "^2.2.6",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^3.0.0",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.notVerified {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
border: solid 1px #dddd00;
|
||||
background: #FFFF9C;
|
||||
color: #777700;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import {Button, Spinner, Success, Alert} from 'plugin-api/beta/client/components/ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ResendVerification.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class ResendVerification extends React.Component {
|
||||
render() {
|
||||
const {resendVerification, error, loading, success, email} = this.props;
|
||||
return (
|
||||
<div className="talk-resend-verification">
|
||||
<h1 className={styles.header}>
|
||||
{t('sign_in.email_verify_cta')}
|
||||
</h1>
|
||||
|
||||
{error &&
|
||||
<Alert>
|
||||
{error.translation_key ? t(`error.${error.translation_key}`) : error.toString()}
|
||||
</Alert>}
|
||||
<div className={styles.notVerified}>
|
||||
{t('error.email_not_verified', email)}
|
||||
</div>
|
||||
<div>
|
||||
<Button id="resendConfirmEmail" cStyle="black" onClick={resendVerification} full>
|
||||
{t('sign_in.request_new_verify_email')}
|
||||
</Button>
|
||||
{loading && <Spinner />}
|
||||
{success && <Success />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ResendVerification.propTypes = {
|
||||
resendVerification: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
loading: PropTypes.bool,
|
||||
success: PropTypes.bool,
|
||||
email: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ResendVerification;
|
||||
@@ -5,13 +5,22 @@ import styles from './styles.css';
|
||||
import SignInContent from './SignInContent';
|
||||
import SignUpContent from './SignUpContent';
|
||||
import ForgotContent from './ForgotContent';
|
||||
import ResendVerification from './ResendVerification';
|
||||
|
||||
const SignDialog = ({open, view, hideSignInDialog, ...props}) => (
|
||||
const SignDialog = ({open, view, resetSignInDialog, ...props}) => (
|
||||
<Dialog className={styles.dialog} id="signInDialog" open={open}>
|
||||
<span className={styles.close} onClick={hideSignInDialog}>×</span>
|
||||
{view !== 'SIGNIN' && <span className={styles.close} onClick={resetSignInDialog}>×</span>}
|
||||
{view === 'SIGNIN' && <SignInContent {...props} />}
|
||||
{view === 'SIGNUP' && <SignUpContent {...props} />}
|
||||
{view === 'FORGOT' && <ForgotContent {...props} />}
|
||||
{view === 'RESEND_VERIFICATION' &&
|
||||
<ResendVerification
|
||||
resendVerification={props.resendVerification}
|
||||
error={props.auth.emailVerificationFailure}
|
||||
success={props.auth.emailVerificationSuccess}
|
||||
loading={props.auth.emailVerificationLoading}
|
||||
email={props.auth.email}
|
||||
/>}
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
fetchSignUpFacebook,
|
||||
fetchForgotPassword,
|
||||
requestConfirmEmail,
|
||||
resetSignInDialog,
|
||||
facebookCallback,
|
||||
invalidForm,
|
||||
validForm,
|
||||
@@ -31,7 +32,6 @@ class SignInContainer extends React.Component {
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
emailToBeResent: '',
|
||||
errors: {},
|
||||
showErrors: false
|
||||
};
|
||||
@@ -80,26 +80,15 @@ class SignInContainer extends React.Component {
|
||||
);
|
||||
};
|
||||
|
||||
handleChangeEmail = (e) => {
|
||||
const {value} = e.target;
|
||||
this.setState({emailToBeResent: value});
|
||||
};
|
||||
|
||||
handleResendVerification = (e) => {
|
||||
e.preventDefault();
|
||||
resendVerification = () => {
|
||||
this.props
|
||||
.requestConfirmEmail(
|
||||
this.state.emailToBeResent,
|
||||
)
|
||||
.requestConfirmEmail(this.props.auth.email)
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
|
||||
// allow success UI to be shown for a second, and then close the modal
|
||||
this.props.hideSignInDialog();
|
||||
this.props.resetSignInDialog();
|
||||
}, 2500);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -194,6 +183,7 @@ const mapDispatchToProps = (dispatch) =>
|
||||
requestConfirmEmail,
|
||||
changeView,
|
||||
hideSignInDialog,
|
||||
resetSignInDialog,
|
||||
invalidForm,
|
||||
validForm
|
||||
},
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, TextField, Spinner, Success, Alert} from 'plugin-api/beta/client/components/ui';
|
||||
import {Button, TextField, Spinner, Alert} from 'plugin-api/beta/client/components/ui';
|
||||
import styles from './styles.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const SignInContent = ({
|
||||
handleChange,
|
||||
handleChangeEmail,
|
||||
emailToBeResent,
|
||||
handleResendVerification,
|
||||
emailVerificationLoading,
|
||||
emailVerificationSuccess,
|
||||
formData,
|
||||
changeView,
|
||||
handleSignIn,
|
||||
@@ -21,71 +16,56 @@ const SignInContent = ({
|
||||
<div className="coral-sign-in">
|
||||
<div className={`${styles.header} header`}>
|
||||
<h1>
|
||||
{auth.emailVerificationFailure
|
||||
? t('sign_in.email_verify_cta')
|
||||
: t('sign_in.sign_in_to_join')}
|
||||
{t('sign_in.sign_in_to_join')}
|
||||
</h1>
|
||||
</div>
|
||||
{auth.error && <Alert>{auth.error}</Alert>}
|
||||
{auth.emailVerificationFailure
|
||||
? <form onSubmit={handleResendVerification}>
|
||||
<p>{t('sign_in.request_new_verify_email')}</p>
|
||||
{auth.error &&
|
||||
<Alert>
|
||||
{auth.error.translation_key ? t(`error.${auth.error.translation_key}`) : auth.error.toString()}
|
||||
</Alert>}
|
||||
<div>
|
||||
<div className={`${styles.socialConnections} social-connections`}>
|
||||
<Button cStyle="facebook" onClick={fetchSignInFacebook} full>
|
||||
{t('sign_in.facebook_sign_in')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h1>
|
||||
{t('sign_in.or')}
|
||||
</h1>
|
||||
</div>
|
||||
<form onSubmit={handleSignIn}>
|
||||
<TextField
|
||||
id="confirm-email"
|
||||
id="email"
|
||||
type="email"
|
||||
label={t('sign_in.email')}
|
||||
value={emailToBeResent}
|
||||
onChange={handleChangeEmail}
|
||||
value={formData.email}
|
||||
style={{fontSize: 16}}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Button id="resendConfirmEmail" type="submit" cStyle="black" full>
|
||||
Send Email
|
||||
</Button>
|
||||
{emailVerificationLoading && <Spinner />}
|
||||
{emailVerificationSuccess && <Success />}
|
||||
<TextField
|
||||
id="password"
|
||||
type="password"
|
||||
label={t('sign_in.password')}
|
||||
value={formData.password}
|
||||
style={{fontSize: 16}}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div className={styles.action}>
|
||||
{!auth.isLoading
|
||||
? <Button
|
||||
id="coralLogInButton"
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
className={styles.signInButton}
|
||||
full
|
||||
>
|
||||
{t('sign_in.sign_in')}
|
||||
</Button>
|
||||
: <Spinner />}
|
||||
</div>
|
||||
</form>
|
||||
: <div>
|
||||
<div className={`${styles.socialConnections} social-connections`}>
|
||||
<Button cStyle="facebook" onClick={fetchSignInFacebook} full>
|
||||
{t('sign_in.facebook_sign_in')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h1>
|
||||
{t('sign_in.or')}
|
||||
</h1>
|
||||
</div>
|
||||
<form onSubmit={handleSignIn}>
|
||||
<TextField
|
||||
id="email"
|
||||
type="email"
|
||||
label={t('sign_in.email')}
|
||||
value={formData.email}
|
||||
style={{fontSize: 16}}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
type="password"
|
||||
label={t('sign_in.password')}
|
||||
value={formData.password}
|
||||
style={{fontSize: 16}}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div className={styles.action}>
|
||||
{!auth.isLoading
|
||||
? <Button
|
||||
id="coralLogInButton"
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
className={styles.signInButton}
|
||||
full
|
||||
>
|
||||
{t('sign_in.sign_in')}
|
||||
</Button>
|
||||
: <Spinner />}
|
||||
</div>
|
||||
</form>
|
||||
</div>}
|
||||
</div>
|
||||
<div className={`${styles.footer} footer`}>
|
||||
<span>
|
||||
<a onClick={() => changeView('FORGOT')}>
|
||||
@@ -111,12 +91,12 @@ SignInContent.propTypes = {
|
||||
}).isRequired,
|
||||
fetchSignInFacebook: PropTypes.func.isRequired,
|
||||
handleSignIn: PropTypes.func.isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
changeView: PropTypes.func.isRequired,
|
||||
emailVerificationLoading: PropTypes.bool.isRequired,
|
||||
emailVerificationSuccess: PropTypes.bool.isRequired,
|
||||
handleResendVerification: PropTypes.func.isRequired,
|
||||
handleChangeEmail: PropTypes.func.isRequired,
|
||||
emailToBeResent: PropTypes.string.isRequired
|
||||
resendVerification: PropTypes.func.isRequired,
|
||||
formData: PropTypes.object,
|
||||
};
|
||||
|
||||
export default SignInContent;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
en:
|
||||
sign_in:
|
||||
email_verify_cta: "Please verify your email address."
|
||||
request_new_verify_email: "Request another email:"
|
||||
request_new_verify_email: "Request another email"
|
||||
verify_email: "Thank you for creating an account! We sent an email to the address you provided to verify your account."
|
||||
verify_email2: "You must verify your account before engaging with the community."
|
||||
not_you: "Not you?"
|
||||
@@ -45,7 +45,7 @@ en:
|
||||
es:
|
||||
sign_in:
|
||||
email_verify_cta: "Por favor confirme su correo."
|
||||
request_new_verify_email: "Enviar otro correo:"
|
||||
request_new_verify_email: "Enviar otro correo"
|
||||
verify_email: "¡Gracias por crear una cuenta! Le enviamos un correo a la direcció\
|
||||
n que dio para confirmar su cuenta."
|
||||
verify_email2: "Debe confirmarla antes de poder involucrarse en la comunidad."
|
||||
@@ -92,7 +92,7 @@ es:
|
||||
fr:
|
||||
sign_in:
|
||||
email_verify_cta: "Merci de vérifier votre adresse e-mail."
|
||||
request_new_verify_email: "Demander un nouvel envoi d'e-mail :"
|
||||
request_new_verify_email: "Demander un nouvel envoi d'e-mail"
|
||||
verify_email: "Merci d'avoir créé un compte ! Nous avons envoyé un courrier électronique à l'adresse que vous avez fournie pour vérifier votre adresse e-mail."
|
||||
verify_email2: "Vous devez vérifier votre adresse e-mail avant de vous engager auprès de la communauté."
|
||||
not_you: "Pas vous ?"
|
||||
|
||||
+20
-10
@@ -37,34 +37,44 @@ const FilterOpenAssets = (query, filter) => {
|
||||
router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => {
|
||||
|
||||
const {
|
||||
limit = 20,
|
||||
skip = 0,
|
||||
sort = 'asc',
|
||||
value = '',
|
||||
field = 'created_at',
|
||||
page = 1,
|
||||
asc = 'false',
|
||||
filter = 'all',
|
||||
search = ''
|
||||
limit = 20,
|
||||
} = req.query;
|
||||
|
||||
try {
|
||||
|
||||
const queryOpts = {
|
||||
sort: {[field]: (asc === 'true') ? 1 : -1},
|
||||
skip: (page - 1) * limit,
|
||||
limit
|
||||
};
|
||||
|
||||
// Find all the assets.
|
||||
let [result, count] = await Promise.all([
|
||||
|
||||
// Find the actuall assets.
|
||||
FilterOpenAssets(AssetsService.search({value: search}), filter)
|
||||
.sort({[field]: (sort === 'asc') ? 1 : -1})
|
||||
.skip(parseInt(skip))
|
||||
.limit(parseInt(limit)),
|
||||
FilterOpenAssets(AssetsService.search({value}), filter)
|
||||
.sort(queryOpts.sort)
|
||||
.skip(parseInt(queryOpts.skip))
|
||||
.limit(parseInt(queryOpts.limit))
|
||||
.lean(),
|
||||
|
||||
// Get the count of actual assets.
|
||||
FilterOpenAssets(AssetsService.search({value: search}), filter)
|
||||
FilterOpenAssets(AssetsService.search({value}), filter)
|
||||
.count()
|
||||
]);
|
||||
|
||||
// Send back the asset data.
|
||||
res.json({
|
||||
result,
|
||||
count
|
||||
limit: Number(limit),
|
||||
count,
|
||||
page: Number(page),
|
||||
totalPages: Math.ceil(count / (limit === 0 ? 1 : limit))
|
||||
});
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
||||
@@ -12,6 +12,10 @@ router.get('/', (req, res, next) => {
|
||||
return;
|
||||
}
|
||||
|
||||
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
|
||||
res.header('Expires', '-1');
|
||||
res.header('Pragma', 'no-cache');
|
||||
|
||||
// Send back the user object.
|
||||
res.json({user: req.user});
|
||||
});
|
||||
|
||||
+47
-21
@@ -5,29 +5,37 @@ const mailer = require('../../../services/mailer');
|
||||
const errors = require('../../../errors');
|
||||
const authorization = require('../../../middleware/authorization');
|
||||
const i18n = require('../../../services/i18n');
|
||||
const Limit = require('../../../services/limit');
|
||||
const {
|
||||
ROOT_URL
|
||||
} = require('../../../config');
|
||||
|
||||
// get a list of users.
|
||||
router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => {
|
||||
|
||||
const {
|
||||
value = '',
|
||||
field = 'created_at',
|
||||
page = 1,
|
||||
asc = 'false',
|
||||
limit = 50 // Total Per Page
|
||||
limit = 20 // Total Per Page
|
||||
} = req.query;
|
||||
|
||||
try {
|
||||
|
||||
const queryOpts = {
|
||||
sort: {[field]: (asc === 'true') ? 1 : -1},
|
||||
skip: (page - 1) * limit,
|
||||
limit
|
||||
};
|
||||
|
||||
let [result, count] = await Promise.all([
|
||||
UsersService
|
||||
.search(value)
|
||||
.sort({[field]: (asc === 'true') ? 1 : -1})
|
||||
.skip((page - 1) * limit)
|
||||
.limit(limit),
|
||||
UsersService.count()
|
||||
.sort(queryOpts.sort)
|
||||
.skip(parseInt(queryOpts.skip))
|
||||
.limit(parseInt(queryOpts.limit))
|
||||
.lean(),
|
||||
UsersService.search(value).count()
|
||||
]);
|
||||
|
||||
res.json({
|
||||
@@ -41,7 +49,6 @@ router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, nex
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
router.post('/:user_id/role', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => {
|
||||
@@ -109,16 +116,15 @@ router.post('/:user_id/email', authorization.needed('ADMIN', 'MODERATOR'), async
|
||||
|
||||
/**
|
||||
* SendEmailConfirmation sends a confirmation email to the user.
|
||||
* @param {ExpressApp} app the instance of the express app
|
||||
* @param {String} userID the id for the user to send the email to
|
||||
* @param {String} email the email for the user to send the email to
|
||||
*/
|
||||
const SendEmailConfirmation = async (app, userID, email, referer) => {
|
||||
let token = await UsersService.createEmailConfirmToken(userID, email, referer);
|
||||
const SendEmailConfirmation = async (user, email, referer) => {
|
||||
let token = await UsersService.createEmailConfirmToken(user, email, referer);
|
||||
|
||||
return mailer.sendSimple({
|
||||
template: 'email-confirm', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
template: 'email-confirm',
|
||||
locals: {
|
||||
token,
|
||||
rootURL: ROOT_URL,
|
||||
email
|
||||
@@ -138,7 +144,7 @@ router.post('/', async (req, res, next) => {
|
||||
|
||||
// Send an email confirmation. The Front end will know about the
|
||||
// requireEmailConfirmation as it's included in the settings get endpoint.
|
||||
await SendEmailConfirmation(req.app, user.id, email, redirectUri);
|
||||
await SendEmailConfirmation(user, email, redirectUri);
|
||||
|
||||
res.status(201).json(user);
|
||||
} catch (e) {
|
||||
@@ -166,25 +172,45 @@ router.post('/:user_id/actions', authorization.needed(), async (req, res, next)
|
||||
}
|
||||
});
|
||||
|
||||
// This will allow 1 try every minute.
|
||||
const resendRateLimiter = new Limit('/api/v1/users/resend-verify', 1, '1m');
|
||||
|
||||
// trigger an email confirmation re-send by a new user
|
||||
router.post('/resend-verify', async (req, res, next) => {
|
||||
const {email} = req.body;
|
||||
const redirectUri = req.header('X-Pym-Url') || req.header('Referer');
|
||||
let {email = ''} = req.body;
|
||||
|
||||
if (!email) {
|
||||
// Clean up and validate the email.
|
||||
email = email.toLowerCase().trim();
|
||||
if (email.length < 5) {
|
||||
return next(errors.ErrMissingEmail);
|
||||
}
|
||||
|
||||
// Check if we're past the rate limit, if we are, stop now. Otherwise, record
|
||||
// this as an attempt to send a verification email.
|
||||
try {
|
||||
const tries = await resendRateLimiter.get(email);
|
||||
if (tries > 0) {
|
||||
throw errors.ErrMaxRateLimit;
|
||||
}
|
||||
|
||||
// find user by email.
|
||||
// if the local profile is verified, return an error code?
|
||||
// send a 204 after the email is re-sent
|
||||
await SendEmailConfirmation(req.app, null, email, redirectUri);
|
||||
await resendRateLimiter.test(email);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await UsersService.findLocalUser(email);
|
||||
if (!user) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
await SendEmailConfirmation(user, email, redirectUri);
|
||||
|
||||
res.status(204).end();
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
console.trace(e);
|
||||
res.status(204).end();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -208,7 +234,7 @@ router.post('/:user_id/email/confirm', authorization.needed('ADMIN', 'MODERATOR'
|
||||
}
|
||||
|
||||
// Send the email to the first local profile that was found.
|
||||
await SendEmailConfirmation(req.app, user.id, localProfile.id);
|
||||
await SendEmailConfirmation(user, localProfile.id);
|
||||
|
||||
res.status(204).end();
|
||||
} catch (e) {
|
||||
|
||||
+2
-6
@@ -21,16 +21,12 @@ if [[ "${CIRCLE_BRANCH}" == "master" ]]; then
|
||||
# Test using browserstack.
|
||||
browserstack chrome
|
||||
browserstack firefox
|
||||
|
||||
# temporarily turn off ci, please fix https://www.pivotaltracker.com/story/show/152144406.
|
||||
# browserstack ie
|
||||
browserstack ie
|
||||
browserstack edge
|
||||
|
||||
# Safari >= 8 has issues connecting to browserstack-local. Safari < 8 is too old.
|
||||
# browserstack safari
|
||||
|
||||
# Edge 14 & 15 randomly fails when switching from the login popup back to the main window.
|
||||
# browserstack edge
|
||||
|
||||
exit $exitCode
|
||||
else
|
||||
# When browserstack is not available test locally using chrome headless.
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
const ms = require('ms');
|
||||
const errors = require('../errors');
|
||||
const {createClientFactory} = require('./redis');
|
||||
const client = createClientFactory();
|
||||
|
||||
/**
|
||||
* Limit is designed to support rate limiting a resource.
|
||||
*/
|
||||
class Limit {
|
||||
constructor(prefix, max, duration) {
|
||||
this.ttl = ms(duration) / 1000;
|
||||
this.prefix = prefix;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
/**
|
||||
* key will compose the redis key used to store the rate limit information.
|
||||
*
|
||||
* @param {String} value the string to use that is being limited
|
||||
* @returns {String} the redis key to set
|
||||
*/
|
||||
key(value) {
|
||||
return `limit[${this.prefix}][${value}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* get will fetch the current number of attempts within the given window
|
||||
* duration.
|
||||
*
|
||||
* @param {String} value the value to limit with
|
||||
* @returns {Integer} the number of tries within the current window
|
||||
*/
|
||||
async get(value) {
|
||||
const key = this.key(value);
|
||||
|
||||
return client().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* test will increment the number of tries, reset the window length and
|
||||
* will throw an error if the number of tries exceed the maximum for the
|
||||
* window duration.
|
||||
*
|
||||
* @param {String} value the value to limit with
|
||||
* @returns {Promise} resolves to the number of tries, or throws an error
|
||||
*/
|
||||
async test(value) {
|
||||
const key = this.key(value);
|
||||
|
||||
const [[, tries], [, expiry]] = await client()
|
||||
.multi()
|
||||
.incr(key)
|
||||
.expire(key, this.ttl)
|
||||
.exec();
|
||||
|
||||
// if this is new or has no expiry
|
||||
if (tries === 1 || expiry === -1) {
|
||||
|
||||
// then expire it after the timeout
|
||||
client().expire(key, this.ttl);
|
||||
}
|
||||
|
||||
if (tries > this.max) {
|
||||
throw errors.ErrMaxRateLimit;
|
||||
}
|
||||
|
||||
return tries;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Limit;
|
||||
+5
-1
@@ -64,7 +64,11 @@ const options = {
|
||||
};
|
||||
|
||||
if (SMTP_PORT) {
|
||||
options.port = SMTP_PORT;
|
||||
try {
|
||||
options.port = parseInt(SMTP_PORT);
|
||||
} catch (e) {
|
||||
throw new Error('TALK_SMTP_PORT is not an integer');
|
||||
}
|
||||
} else {
|
||||
options.port = 25;
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ async function ValidateUserLogin(loginProfile, user, done) {
|
||||
// If the profile doesn't have a metadata field, or it does not have a
|
||||
// confirmed_at field, or that field is null, then send them back.
|
||||
if (!profile.metadata || !profile.metadata.confirmed_at || profile.metadata.confirmed_at === null) {
|
||||
return done(new errors.ErrAuthentication(loginProfile.id));
|
||||
return done(errors.ErrNotVerified);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-40
@@ -18,7 +18,7 @@ const UserModel = require('../models/user');
|
||||
const USER_STATUS = require('../models/enum/user_status');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
|
||||
const RECAPTCHA_WINDOW_SECONDS = 60 * 10; // 10 minutes.
|
||||
const RECAPTCHA_WINDOW = '10m'; // 10 minutes.
|
||||
const RECAPTCHA_INCORRECT_TRIGGER = 5; // after 3 incorrect attempts, recaptcha will be required.
|
||||
|
||||
const ActionsService = require('./actions');
|
||||
@@ -35,9 +35,8 @@ const PASSWORD_RESET_JWT_SUBJECT = 'password_reset';
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
// Create a redis client to use for authentication.
|
||||
const {createClientFactory} = require('./redis');
|
||||
|
||||
const client = createClientFactory();
|
||||
const Limit = require('./limit');
|
||||
const loginRateLimiter = new Limit('loginAttempts', RECAPTCHA_INCORRECT_TRIGGER, RECAPTCHA_WINDOW);
|
||||
|
||||
// UsersService is the interface for the application to interact with the
|
||||
// UserModel through.
|
||||
@@ -71,23 +70,14 @@ module.exports = class UsersService {
|
||||
* where future login attempts must be made with the recaptcha flag.
|
||||
*/
|
||||
static async recordLoginAttempt(email) {
|
||||
const rdskey = `la[${email.toLowerCase().trim()}]`;
|
||||
try {
|
||||
await loginRateLimiter.test(email.toLowerCase().trim());
|
||||
} catch (err) {
|
||||
if (err === errors.ErrMaxRateLimit) {
|
||||
throw errors.ErrLoginAttemptMaximumExceeded;
|
||||
}
|
||||
|
||||
const replies = await client()
|
||||
.multi()
|
||||
.incr(rdskey)
|
||||
.expire(rdskey, RECAPTCHA_WINDOW_SECONDS)
|
||||
.exec();
|
||||
|
||||
// if this is new or has no expiry
|
||||
if (replies[0] === 1 || replies[1] === -1) {
|
||||
|
||||
// then expire it after the timeout
|
||||
client().expire(rdskey, RECAPTCHA_WINDOW_SECONDS);
|
||||
}
|
||||
|
||||
if (replies[0] >= RECAPTCHA_INCORRECT_TRIGGER) {
|
||||
throw errors.ErrLoginAttemptMaximumExceeded;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +88,7 @@ module.exports = class UsersService {
|
||||
* errors.ErrLoginAttemptMaximumExceeded
|
||||
*/
|
||||
static async checkLoginAttempts(email) {
|
||||
const rdskey = `la[${email.toLowerCase().trim()}]`;
|
||||
|
||||
const attempts = await client().get(rdskey);
|
||||
const attempts = await loginRateLimiter.get(email.toLowerCase().trim());
|
||||
if (!attempts) {
|
||||
return;
|
||||
}
|
||||
@@ -668,8 +656,8 @@ module.exports = class UsersService {
|
||||
* Returns a count of the current users.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static count() {
|
||||
return UserModel.count();
|
||||
static count(query = {}) {
|
||||
return UserModel.count(query);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -718,7 +706,7 @@ module.exports = class UsersService {
|
||||
* @param {String} email The email that we are needing to get confirmed.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async createEmailConfirmToken(userID = null, email, referer = ROOT_URL) {
|
||||
static async createEmailConfirmToken(user, email, referer = ROOT_URL) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
throw new Error('email is required when creating a JWT for resetting passord');
|
||||
}
|
||||
@@ -732,20 +720,6 @@ module.exports = class UsersService {
|
||||
subject: EMAIL_CONFIRM_JWT_SUBJECT
|
||||
};
|
||||
|
||||
let user;
|
||||
if (!userID) {
|
||||
|
||||
// If there is no userID, we're coming from the endpoint where a new user
|
||||
// is re-requesting a confirmation email and we don't know the userID.
|
||||
user = await UserModel.findOne({profiles: {$elemMatch: {id: email, provider: 'local'}}});
|
||||
} else {
|
||||
user = await UsersService.findById(userID);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
// Get the profile representing the local account.
|
||||
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
|
||||
|
||||
|
||||
@@ -14,5 +14,10 @@ module.exports = {
|
||||
'emailInput': '.talk-admin-login-sign-in #email',
|
||||
'passwordInput': '.talk-admin-login-sign-in #password',
|
||||
'signInButton': '.talk-admin-login-sign-in-button',
|
||||
'storiesNav': '.talk-admin-nav-stories',
|
||||
'storiesSection': '.talk-admin-stories',
|
||||
'communityNav': '.talk-admin-nav-community',
|
||||
'communitySection': '.talk-admin-community',
|
||||
'moderationContainer': '.talk-admin-moderation-container'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
module.exports = {
|
||||
'@tags': ['admin', 'login'],
|
||||
beforeEach: (client) => {
|
||||
|
||||
// Testing Desktop
|
||||
client.resizeWindow(1280, 800);
|
||||
},
|
||||
'Admin logs in': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
const {testData: {admin}} = client.globals;
|
||||
@@ -14,6 +18,30 @@ module.exports = {
|
||||
.waitForElementVisible('@signInButton')
|
||||
.click('@signInButton');
|
||||
|
||||
client.pause(3000);
|
||||
|
||||
adminPage
|
||||
.waitForElementVisible('@moderationContainer');
|
||||
},
|
||||
|
||||
'Admin goes to Stories': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
|
||||
adminPage
|
||||
.navigate()
|
||||
.waitForElementVisible('@storiesNav')
|
||||
.click('@storiesNav')
|
||||
.waitForElementVisible('@storiesSection');
|
||||
},
|
||||
|
||||
'Admin goes to Community': (client) => {
|
||||
const adminPage = client.page.admin();
|
||||
|
||||
adminPage
|
||||
.navigate()
|
||||
.waitForElementVisible('@communityNav')
|
||||
.click('@communityNav')
|
||||
.waitForElementVisible('@communitySection');
|
||||
},
|
||||
after: (client) => {
|
||||
client.end();
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('/api/v1/assets', () => {
|
||||
it('should return assets that we search for', async () => {
|
||||
for (const role of ['ADMIN', 'MODERATOR']) {
|
||||
const res = await chai.request(app)
|
||||
.get('/api/v1/assets?search=term2')
|
||||
.get('/api/v1/assets?value=term2')
|
||||
.set(passport.inject({roles: [role]}));
|
||||
|
||||
const body = res.body;
|
||||
@@ -78,7 +78,7 @@ describe('/api/v1/assets', () => {
|
||||
it('should not return assets that we do not search for', async () => {
|
||||
for (const role of ['ADMIN', 'MODERATOR']) {
|
||||
const res = await chai.request(app)
|
||||
.get('/api/v1/assets?search=term3')
|
||||
.get('/api/v1/assets?value=term3')
|
||||
.set(passport.inject({roles: [role]}));
|
||||
const body = res.body;
|
||||
|
||||
|
||||
@@ -74,10 +74,8 @@ describe('/api/v1/auth/local', () => {
|
||||
.catch((err) => {
|
||||
expect(err).to.have.status(401);
|
||||
err.response.body.should.have.property('error');
|
||||
err.response.body.error.should.have.property('metadata');
|
||||
err.response.body.error.metadata.should.have.property('message', 'maria@gmail.com');
|
||||
|
||||
return UsersService.createEmailConfirmToken(mockUser.id, mockUser.profiles[0].id);
|
||||
return UsersService.createEmailConfirmToken(mockUser, mockUser.profiles[0].id);
|
||||
})
|
||||
.then(UsersService.verifyEmailConfirmation)
|
||||
.then(() => {
|
||||
|
||||
@@ -88,17 +88,19 @@ describe('services.UsersService', () => {
|
||||
describe('#createEmailConfirmToken', () => {
|
||||
|
||||
it('should create a token for a valid user', async () => {
|
||||
const token = await UsersService.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id);
|
||||
const token = await UsersService.createEmailConfirmToken(mockUsers[0], mockUsers[0].profiles[0].id);
|
||||
expect(token).to.not.be.null;
|
||||
});
|
||||
|
||||
it('should not create a token for a user already verified', async () => {
|
||||
const token = await UsersService.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id);
|
||||
const token = await UsersService.createEmailConfirmToken(mockUsers[0], mockUsers[0].profiles[0].id);
|
||||
expect(token).to.not.be.null;
|
||||
|
||||
await UsersService.verifyEmailConfirmation(token);
|
||||
|
||||
return expect(UsersService.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)).to.eventually.be.rejected;
|
||||
const user = await UsersService.findById(mockUsers[0].id);
|
||||
|
||||
return expect(UsersService.createEmailConfirmToken(user, mockUsers[0].profiles[0].id)).to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
});
|
||||
@@ -106,7 +108,7 @@ describe('services.UsersService', () => {
|
||||
describe('#verifyEmailConfirmation', () => {
|
||||
|
||||
it('should correctly validate a valid token', async () => {
|
||||
const token = await UsersService.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id);
|
||||
const token = await UsersService.createEmailConfirmToken(mockUsers[0], mockUsers[0].profiles[0].id);
|
||||
expect(token).to.not.be.null;
|
||||
|
||||
return expect(UsersService.verifyEmailConfirmation(token)).to.eventually.not.be.rejected;
|
||||
@@ -122,7 +124,7 @@ describe('services.UsersService', () => {
|
||||
|
||||
it('should update the user model when verification is complete', () => {
|
||||
return UsersService
|
||||
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
|
||||
.createEmailConfirmToken(mockUsers[0], mockUsers[0].profiles[0].id)
|
||||
.then((token) => {
|
||||
expect(token).to.not.be.null;
|
||||
|
||||
|
||||
@@ -72,11 +72,7 @@ abab@^1.0.0, abab@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
||||
abbrev@1.0.x:
|
||||
abbrev@1, abbrev@1.0.x:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
|
||||
|
||||
@@ -403,18 +399,12 @@ async@2.1.4:
|
||||
dependencies:
|
||||
lodash "^4.14.0"
|
||||
|
||||
async@2.4.1:
|
||||
async@2.4.1, async@^2.1.2, async@^2.1.4:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
|
||||
dependencies:
|
||||
lodash "^4.14.0"
|
||||
|
||||
async@^2.1.2, async@^2.1.4:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
|
||||
dependencies:
|
||||
lodash "^4.14.0"
|
||||
|
||||
async@~0.9.0:
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
|
||||
@@ -1933,14 +1923,10 @@ cookie@0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
|
||||
|
||||
cookiejar@2.0.x:
|
||||
cookiejar@2.0.x, cookiejar@^2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.0.6.tgz#0abf356ad00d1c5a219d88d44518046dd026acfe"
|
||||
|
||||
cookiejar@^2.0.6:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a"
|
||||
|
||||
copy-webpack-plugin@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.1.1.tgz#53ae69e04955ebfa9fda411f54cbb968531d71fd"
|
||||
@@ -2375,14 +2361,10 @@ diff@1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf"
|
||||
|
||||
diff@3.2.0:
|
||||
diff@3.2.0, diff@^3.1.0, diff@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
|
||||
|
||||
diff@^3.1.0, diff@^3.2.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
|
||||
|
||||
diffie-hellman@^5.0.0:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
|
||||
@@ -2425,40 +2407,27 @@ domain-browser@^1.1.1:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
|
||||
|
||||
domelementtype@1, domelementtype@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
|
||||
|
||||
domelementtype@~1.1.1:
|
||||
domelementtype@1, domelementtype@~1.1.1:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
|
||||
|
||||
domhandler@2.3:
|
||||
domelementtype@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
|
||||
|
||||
domhandler@2.3, domhandler@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738"
|
||||
dependencies:
|
||||
domelementtype "1"
|
||||
|
||||
domhandler@^2.3.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259"
|
||||
dependencies:
|
||||
domelementtype "1"
|
||||
|
||||
domutils@1.5, domutils@1.5.1:
|
||||
domutils@1.5, domutils@1.5.1, domutils@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
|
||||
dependencies:
|
||||
dom-serializer "0"
|
||||
domelementtype "1"
|
||||
|
||||
domutils@^1.5.1:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff"
|
||||
dependencies:
|
||||
dom-serializer "0"
|
||||
domelementtype "1"
|
||||
|
||||
dont-sniff-mimetype@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz#5932890dc9f4e2f19e5eb02a20026e5e5efc8f58"
|
||||
@@ -2890,7 +2859,7 @@ exports-loader@^0.6.4:
|
||||
loader-utils "^1.0.2"
|
||||
source-map "0.5.x"
|
||||
|
||||
express@4.16.0:
|
||||
express@4.16.0, express@^4.12.2:
|
||||
version "4.16.0"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.16.0.tgz#b519638e4eb58e7178c81b498ef22f798cb2e255"
|
||||
dependencies:
|
||||
@@ -2925,41 +2894,6 @@ express@4.16.0:
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
express@^4.12.2:
|
||||
version "4.16.2"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c"
|
||||
dependencies:
|
||||
accepts "~1.3.4"
|
||||
array-flatten "1.1.1"
|
||||
body-parser "1.18.2"
|
||||
content-disposition "0.5.2"
|
||||
content-type "~1.0.4"
|
||||
cookie "0.3.1"
|
||||
cookie-signature "1.0.6"
|
||||
debug "2.6.9"
|
||||
depd "~1.1.1"
|
||||
encodeurl "~1.0.1"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
finalhandler "1.1.0"
|
||||
fresh "0.5.2"
|
||||
merge-descriptors "1.0.1"
|
||||
methods "~1.1.2"
|
||||
on-finished "~2.3.0"
|
||||
parseurl "~1.3.2"
|
||||
path-to-regexp "0.1.7"
|
||||
proxy-addr "~2.0.2"
|
||||
qs "6.5.1"
|
||||
range-parser "~1.2.0"
|
||||
safe-buffer "5.1.1"
|
||||
send "0.16.1"
|
||||
serve-static "1.13.1"
|
||||
setprototypeof "1.1.0"
|
||||
statuses "~1.3.1"
|
||||
type-is "~1.6.15"
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
extend@3, extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
||||
@@ -3008,7 +2942,7 @@ fb-watchman@^2.0.0:
|
||||
dependencies:
|
||||
bser "^2.0.0"
|
||||
|
||||
fbjs@^0.8.1, fbjs@^0.8.16, fbjs@^0.8.9:
|
||||
fbjs@^0.8.1, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.9:
|
||||
version "0.8.16"
|
||||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
|
||||
dependencies:
|
||||
@@ -4047,7 +3981,7 @@ inflight@^1.0.4:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
|
||||
inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
|
||||
@@ -4185,7 +4119,7 @@ ip@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-1.0.1.tgz#c7e356cdea225ae71b36d70f2e71a92ba4e42590"
|
||||
|
||||
ip@^1.1.2, ip@^1.1.4:
|
||||
ip@^1.1.2:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
|
||||
|
||||
@@ -5515,9 +5449,9 @@ lowercase-keys@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
|
||||
|
||||
lru-cache@^2.5.0:
|
||||
version "2.7.3"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952"
|
||||
lru-cache@^2.5.0, lru-cache@~2.6.5:
|
||||
version "2.6.5"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5"
|
||||
|
||||
lru-cache@^4.0.1:
|
||||
version "4.1.1"
|
||||
@@ -5526,10 +5460,6 @@ lru-cache@^4.0.1:
|
||||
pseudomap "^1.0.2"
|
||||
yallist "^2.1.2"
|
||||
|
||||
lru-cache@~2.6.5:
|
||||
version "2.6.5"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5"
|
||||
|
||||
macaddress@^0.2.8:
|
||||
version "0.2.8"
|
||||
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
|
||||
@@ -5707,7 +5637,7 @@ minimatch@3.0.3:
|
||||
dependencies:
|
||||
brace-expansion "^1.0.0"
|
||||
|
||||
minimist@0.0.8:
|
||||
minimist@0.0.8, minimist@~0.0.1:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
|
||||
@@ -5715,10 +5645,6 @@ minimist@^1.1.1, minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
minimist@~0.0.1:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
|
||||
|
||||
mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||
@@ -7115,7 +7041,7 @@ promised-io@*:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/promised-io/-/promised-io-0.3.5.tgz#4ad217bb3658bcaae9946b17a8668ecd851e1356"
|
||||
|
||||
prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8:
|
||||
prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0:
|
||||
version "15.6.0"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
|
||||
dependencies:
|
||||
@@ -7387,6 +7313,14 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-addons-create-fragment@^15.0.0:
|
||||
version "15.6.2"
|
||||
resolved "https://registry.yarnpkg.com/react-addons-create-fragment/-/react-addons-create-fragment-15.6.2.tgz#a394de7c2c7becd6b5475ba1b97ac472ce7c74f8"
|
||||
dependencies:
|
||||
fbjs "^0.8.4"
|
||||
loose-envify "^1.3.1"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
react-apollo@^1.4.12:
|
||||
version "1.4.16"
|
||||
resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-1.4.16.tgz#62a623458b67a174ff8ef25f64e7b42531518e19"
|
||||
@@ -7435,6 +7369,14 @@ react-mdl@^1.7.1, react-mdl@^1.7.2:
|
||||
lodash.isequal "^4.4.0"
|
||||
prop-types "^15.5.0"
|
||||
|
||||
react-paginate@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-paginate/-/react-paginate-5.0.0.tgz#b5c12191ea81adc6d4d1b339b805e81841eaa8ea"
|
||||
dependencies:
|
||||
classnames "^2.2.5"
|
||||
prop-types "^15.6.0"
|
||||
react-addons-create-fragment "^15.0.0"
|
||||
|
||||
react-recaptcha@^2.2.6:
|
||||
version "2.3.5"
|
||||
resolved "https://registry.yarnpkg.com/react-recaptcha/-/react-recaptcha-2.3.5.tgz#a5db337125bb00fb13c2fa2e4ebfbe8b0cd06bb7"
|
||||
@@ -7466,20 +7408,13 @@ react-tagsinput@^3.17.0:
|
||||
version "3.18.0"
|
||||
resolved "https://registry.yarnpkg.com/react-tagsinput/-/react-tagsinput-3.18.0.tgz#40e036fc0f4c3d6b4689858189ab02926717a818"
|
||||
|
||||
react-test-renderer@15.5:
|
||||
react-test-renderer@15.5, react-test-renderer@^15.5.0:
|
||||
version "15.5.4"
|
||||
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.5.4.tgz#d4ebb23f613d685ea8f5390109c2d20fbf7c83bc"
|
||||
dependencies:
|
||||
fbjs "^0.8.9"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
react-test-renderer@^15.5.0:
|
||||
version "15.6.2"
|
||||
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.6.2.tgz#d0333434fc2c438092696ca770da5ed48037efa8"
|
||||
dependencies:
|
||||
fbjs "^0.8.9"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
react-test-renderer@^16.0.0-0:
|
||||
version "16.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.0.0.tgz#9fe7b8308f2f71f29fc356d4102086f131c9cb15"
|
||||
@@ -7550,7 +7485,7 @@ read-pkg@^2.0.0:
|
||||
normalize-package-data "^2.3.2"
|
||||
path-type "^2.0.0"
|
||||
|
||||
readable-stream@1.1:
|
||||
readable-stream@1.1, readable-stream@1.1.x:
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e"
|
||||
dependencies:
|
||||
@@ -7559,28 +7494,7 @@ readable-stream@1.1:
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readable-stream@1.1.x:
|
||||
version "1.1.14"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.1"
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~1.0.6"
|
||||
safe-buffer "~5.1.1"
|
||||
string_decoder "~1.0.3"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readable-stream@2.2.7:
|
||||
readable-stream@2, readable-stream@2.2.7, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6:
|
||||
version "2.2.7"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.7.tgz#07057acbe2467b22042d36f98c5ad507054e95b1"
|
||||
dependencies:
|
||||
@@ -7991,11 +7905,11 @@ rx@^2.4.3:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/rx/-/rx-2.5.3.tgz#21adc7d80f02002af50dae97fd9dbf248755f566"
|
||||
|
||||
safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||
|
||||
samsam@1.1.2:
|
||||
samsam@1.1.2, samsam@~1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567"
|
||||
|
||||
@@ -8003,10 +7917,6 @@ samsam@1.x, samsam@^1.1.3:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
|
||||
|
||||
samsam@~1.1:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621"
|
||||
|
||||
sane@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/sane/-/sane-2.2.0.tgz#d6d2e2fcab00e3d283c93b912b7c3a20846f1d56"
|
||||
@@ -8097,24 +8007,6 @@ send@0.16.0:
|
||||
range-parser "~1.2.0"
|
||||
statuses "~1.3.1"
|
||||
|
||||
send@0.16.1:
|
||||
version "0.16.1"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3"
|
||||
dependencies:
|
||||
debug "2.6.9"
|
||||
depd "~1.1.1"
|
||||
destroy "~1.0.4"
|
||||
encodeurl "~1.0.1"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
fresh "0.5.2"
|
||||
http-errors "~1.6.2"
|
||||
mime "1.4.1"
|
||||
ms "2.0.0"
|
||||
on-finished "~2.3.0"
|
||||
range-parser "~1.2.0"
|
||||
statuses "~1.3.1"
|
||||
|
||||
serve-static@1.13.0:
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.0.tgz#810c91db800e94ba287eae6b4e06caab9fdc16f1"
|
||||
@@ -8124,15 +8016,6 @@ serve-static@1.13.0:
|
||||
parseurl "~1.3.2"
|
||||
send "0.16.0"
|
||||
|
||||
serve-static@1.13.1:
|
||||
version "1.13.1"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719"
|
||||
dependencies:
|
||||
encodeurl "~1.0.1"
|
||||
escape-html "~1.0.3"
|
||||
parseurl "~1.3.2"
|
||||
send "0.16.1"
|
||||
|
||||
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
@@ -8261,7 +8144,7 @@ sliced@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41"
|
||||
|
||||
smart-buffer@^1.0.13, smart-buffer@^1.0.4:
|
||||
smart-buffer@^1.0.4:
|
||||
version "1.1.15"
|
||||
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16"
|
||||
|
||||
@@ -8302,20 +8185,13 @@ socks-proxy-agent@2:
|
||||
extend "3"
|
||||
socks "~1.1.5"
|
||||
|
||||
socks@1.1.9:
|
||||
socks@1.1.9, socks@~1.1.5:
|
||||
version "1.1.9"
|
||||
resolved "https://registry.yarnpkg.com/socks/-/socks-1.1.9.tgz#628d7e4d04912435445ac0b6e459376cb3e6d691"
|
||||
dependencies:
|
||||
ip "^1.1.2"
|
||||
smart-buffer "^1.0.4"
|
||||
|
||||
socks@~1.1.5:
|
||||
version "1.1.10"
|
||||
resolved "https://registry.yarnpkg.com/socks/-/socks-1.1.10.tgz#5b8b7fc7c8f341c53ed056e929b7bf4de8ba7b5a"
|
||||
dependencies:
|
||||
ip "^1.1.4"
|
||||
smart-buffer "^1.0.13"
|
||||
|
||||
sort-keys@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
|
||||
@@ -8463,7 +8339,7 @@ string_decoder@^0.10.25, string_decoder@~0.10.x:
|
||||
version "0.10.31"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
||||
|
||||
string_decoder@~1.0.0, string_decoder@~1.0.3:
|
||||
string_decoder@~1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user