mirror of
https://github.com/wassname/talk.git
synced 2026-07-01 21:49:58 +08:00
Merge branch 'master' into use-talk-linting-preset
Conflicts: package.json
This commit is contained in:
@@ -6,13 +6,9 @@ const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const {
|
||||
BASE_URL,
|
||||
BASE_PATH,
|
||||
MOUNT_PATH,
|
||||
STATIC_URL,
|
||||
HELMET_CONFIGURATION,
|
||||
} = require('./url');
|
||||
const {HELMET_CONFIGURATION} = require('./config');
|
||||
const {MOUNT_PATH} = require('./url');
|
||||
const {applyLocals} = require('./services/locals');
|
||||
const routes = require('./routes');
|
||||
const debug = require('debug')('talk:app');
|
||||
|
||||
@@ -57,12 +53,8 @@ app.set('view engine', 'ejs');
|
||||
// ROUTES
|
||||
//==============================================================================
|
||||
|
||||
// Apply the BASE_PATH, BASE_URL, and MOUNT_PATH on the app.locals, which will
|
||||
// make them available on the templates and the routers.
|
||||
app.locals.BASE_URL = BASE_URL;
|
||||
app.locals.BASE_PATH = BASE_PATH;
|
||||
app.locals.MOUNT_PATH = MOUNT_PATH;
|
||||
app.locals.STATIC_URL = STATIC_URL;
|
||||
// Add the locals to the app renderer.
|
||||
applyLocals(app.locals);
|
||||
|
||||
debug(`mounting routes on the ${MOUNT_PATH} path`);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import Configure from 'routes/Configure';
|
||||
import Dashboard from 'routes/Dashboard';
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import {CommunityLayout, Community} from 'routes/Community';
|
||||
import Community from 'routes/Community/containers/Community';
|
||||
import {ModerationLayout, Moderation} from 'routes/Moderation';
|
||||
|
||||
import Layout from 'containers/Layout';
|
||||
@@ -22,7 +22,7 @@ const routes = (
|
||||
|
||||
{/* Community Routes */}
|
||||
|
||||
<Route path='community' component={CommunityLayout}>
|
||||
<Route path='community'>
|
||||
<Route path='flagged' components={Community}>
|
||||
<Route path=':id' components={Community} />
|
||||
</Route>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import qs from 'qs';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import {
|
||||
FETCH_COMMENTERS_REQUEST,
|
||||
@@ -17,9 +17,8 @@ import {
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => {
|
||||
|
||||
dispatch(requestFetchAccounts());
|
||||
rest(`/users?${qs.stringify(query)}`)
|
||||
rest(`/users?${queryString.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>{
|
||||
dispatch({
|
||||
type: FETCH_COMMENTERS_SUCCESS,
|
||||
|
||||
@@ -6,101 +6,110 @@ import styles from './Header.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {Logo} from './Logo';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import Indicator from './Indicator';
|
||||
|
||||
const CoralHeader = ({
|
||||
handleLogout,
|
||||
showShortcuts = () => {},
|
||||
auth
|
||||
}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
id='moderateNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<Link
|
||||
id='streamsNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
<Link
|
||||
id='communityNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.community')}
|
||||
</Link>
|
||||
{
|
||||
can(auth.user, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</Navigation>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank" rel="noopener noreferrer">
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
auth,
|
||||
root
|
||||
}) => {
|
||||
return (
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
id='moderateNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
{(root.premodCount !== 0 || root.reportedCount !== 0) && <Indicator />}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<Link
|
||||
id='streamsNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
id='communityNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.community')}
|
||||
{root.flaggedUsernamesCount !== 0 && <Indicator />}
|
||||
</Link>
|
||||
|
||||
{
|
||||
can(auth.user, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</Navigation>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank" rel="noopener noreferrer">
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default CoralHeader;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.indicator {
|
||||
background-color: #E46D59;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-top: -4px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import styles from './Indicator.css';
|
||||
|
||||
const Indicator = () =>
|
||||
<span className={styles.indicator}></span>;
|
||||
|
||||
export default Indicator;
|
||||
@@ -1,22 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Layout as LayoutMDL} from 'react-mdl';
|
||||
import Header from './Header';
|
||||
import Header from '../../containers/Header';
|
||||
import Drawer from './Drawer';
|
||||
import styles from './Layout.css';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
handleLogout = () => {},
|
||||
toggleShortcutModal,
|
||||
toggleShortcutModal = () => {},
|
||||
restricted = false,
|
||||
...props}) => (
|
||||
auth,
|
||||
}) => (
|
||||
<LayoutMDL className={styles.layout} fixedDrawer>
|
||||
<Header
|
||||
handleLogout={handleLogout}
|
||||
showShortcuts={toggleShortcutModal}
|
||||
{...props} />
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} {...props} />
|
||||
auth={auth}
|
||||
/>
|
||||
<Drawer
|
||||
handleLogout={handleLogout}
|
||||
restricted={restricted}
|
||||
/>
|
||||
<div className={styles.layout}>
|
||||
{children}
|
||||
</div>
|
||||
@@ -24,6 +29,8 @@ const Layout = ({
|
||||
);
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
auth: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
restricted: PropTypes.bool // hide elements from a user that's logged out
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import styles from './Logo.css';
|
||||
import {CoralLogo} from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Logo = ({className = ''}) => (
|
||||
<div className={`${styles.logo} ${className}`}>
|
||||
@@ -10,3 +11,7 @@ export const Logo = ({className = ''}) => (
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
|
||||
Logo.propTypes = {
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import Header from '../components/ui/Header';
|
||||
|
||||
export default withQuery(gql`
|
||||
query TalkAdmin_Header {
|
||||
__typename
|
||||
premodCount: commentCount(query: {
|
||||
statuses: [PREMOD]
|
||||
})
|
||||
reportedCount: commentCount(query: {
|
||||
statuses: [NONE, PREMOD, SYSTEM_WITHHELD],
|
||||
action_type: FLAG
|
||||
})
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
}
|
||||
`, {
|
||||
options: {
|
||||
pollInterval: 5000
|
||||
}
|
||||
})(Header);
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {Component} from 'react';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import Layout from '../components/ui/Layout';
|
||||
import {fetchConfig} from '../actions/config';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
@@ -10,15 +11,18 @@ import {toggleModal as toggleShortcutModal} from '../actions/moderation';
|
||||
import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import UserDetail from 'coral-admin/src/containers/UserDetail';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class LayoutContainer extends Component {
|
||||
class LayoutContainer extends React.Component {
|
||||
componentWillMount() {
|
||||
const {checkLogin, fetchConfig} = this.props;
|
||||
|
||||
checkLogin();
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const {
|
||||
user,
|
||||
loggedIn,
|
||||
@@ -29,13 +33,16 @@ class LayoutContainer extends Component {
|
||||
} = this.props.auth;
|
||||
|
||||
const {
|
||||
handleLogout,
|
||||
children,
|
||||
logout,
|
||||
toggleShortcutModal,
|
||||
TALK_RECAPTCHA_PUBLIC
|
||||
TALK_RECAPTCHA_PUBLIC,
|
||||
} = this.props;
|
||||
|
||||
if (loadingUser) {
|
||||
return <FullLoading />;
|
||||
}
|
||||
|
||||
if (!loggedIn) {
|
||||
return (
|
||||
<AdminLogin
|
||||
@@ -48,17 +55,17 @@ class LayoutContainer extends Component {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (can(user, 'ACCESS_ADMIN') && loggedIn) {
|
||||
return (
|
||||
<Layout
|
||||
handleLogout={handleLogout}
|
||||
handleLogout={logout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
{...this.props}
|
||||
>
|
||||
auth={this.props.auth} >
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{this.props.children}
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
@@ -72,19 +79,32 @@ class LayoutContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
LayoutContainer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
requestPasswordReset: PropTypes.func,
|
||||
handleLogin: PropTypes.func,
|
||||
auth: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
logout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
|
||||
checkLogin: PropTypes.func,
|
||||
fetchConfig: PropTypes.func
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth,
|
||||
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
checkLogin: () => dispatch(checkLogin()),
|
||||
fetchConfig: () => dispatch(fetchConfig()),
|
||||
handleLogin: (username, password, recaptchaResponse) =>
|
||||
dispatch(handleLogin(username, password, recaptchaResponse)),
|
||||
requestPasswordReset: (email) => dispatch(requestPasswordReset(email)),
|
||||
toggleShortcutModal: (toggle) => dispatch(toggleShortcutModal(toggle)),
|
||||
handleLogout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
checkLogin,
|
||||
fetchConfig,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
toggleShortcutModal,
|
||||
logout
|
||||
}, dispatch);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
|
||||
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
mutations: {
|
||||
SetUserStatus: ({variables: {status, userId}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_FlaggedAccounts: (prev) => {
|
||||
TalkAdmin_Community: (prev) => {
|
||||
if (status !== 'APPROVED') {
|
||||
return prev;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
}),
|
||||
RejectUsername: ({variables: {input: {id: userId}}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_FlaggedAccounts: (prev) => {
|
||||
TalkAdmin_Community: (prev) => {
|
||||
const updated = update(prev, {
|
||||
users: {
|
||||
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const CommunityLayout = (props) => (
|
||||
<div>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default CommunityLayout;
|
||||
@@ -1,16 +1,14 @@
|
||||
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 PropTypes from 'prop-types';
|
||||
|
||||
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 {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
|
||||
import {
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
@@ -27,7 +25,7 @@ class CommunityContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Community
|
||||
return <Community
|
||||
fetchAccounts={this.props.fetchAccounts}
|
||||
community={this.props.community}
|
||||
hideRejectUsernameDialog={this.props.hideRejectUsernameDialog}
|
||||
@@ -56,28 +54,6 @@ CommunityContainer.propTypes = {
|
||||
root: PropTypes.object
|
||||
};
|
||||
|
||||
const withData = withQuery(gql`
|
||||
query TalkAdmin_FlaggedUsernamesCount {
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
...${getDefinitionName(FlaggedAccounts.fragments.root)}
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
me {
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
${FlaggedAccounts.fragments.root}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.me}
|
||||
`, {
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
fetchAccounts,
|
||||
@@ -86,9 +62,27 @@ const mapDispatchToProps = (dispatch) =>
|
||||
newPage,
|
||||
}, dispatch);
|
||||
|
||||
const withData = withQuery(gql`
|
||||
query TalkAdmin_Community {
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
...${getDefinitionName(FlaggedAccounts.fragments.root)}
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
me {
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
${FlaggedAccounts.fragments.root}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.me}
|
||||
`);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetUserStatus,
|
||||
withRejectUsername,
|
||||
withData,
|
||||
withData
|
||||
)(CommunityContainer);
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export {default as Community} from './containers/Community';
|
||||
export {default as CommunityLayout} from './components/CommunityLayout';
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import {Button, Checkbox} from 'coral-ui';
|
||||
import QuestionBoxBuilder from './QuestionBoxBuilder';
|
||||
import cn from 'classnames';
|
||||
|
||||
import styles from './ConfigureCommentStream.css';
|
||||
|
||||
@@ -13,7 +14,7 @@ export default ({handleChange, handleApply, changed, ...props}) => (
|
||||
<h3>{t('configure.title')}</h3>
|
||||
<Button
|
||||
type="submit"
|
||||
className={styles.apply}
|
||||
className={cn(styles.apply, 'talk-embed-stream-configuration-submit-button')}
|
||||
onChange={handleChange}
|
||||
cStyle={changed ? 'green' : 'darkGrey'} >
|
||||
{t('configure.apply')}
|
||||
|
||||
@@ -102,7 +102,7 @@ class ConfigureStreamContainer extends Component {
|
||||
const closedTimeout = dirtySettings.closedTimeout;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='talk-embed-stream-configuration-container'>
|
||||
<ConfigureCommentStream
|
||||
handleChange={this.handleChange}
|
||||
handleApply={this.handleApply}
|
||||
|
||||
@@ -16,7 +16,7 @@ import mapValues from 'lodash/mapValues';
|
||||
import LoadMore from './LoadMore';
|
||||
import {getEditableUntilDate} from './util';
|
||||
import {findCommentWithId} from '../graphql/utils';
|
||||
import CommentContent from './CommentContent';
|
||||
import CommentContent from 'coral-framework/components/CommentContent';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentTombstone from './CommentTombstone';
|
||||
import InactiveCommentLabel from './InactiveCommentLabel';
|
||||
@@ -529,7 +529,7 @@ export default class Comment extends React.Component {
|
||||
<div className={cn(styles.footer, 'talk-stream-comment-footer')}>
|
||||
{isActive &&
|
||||
<div className={'talk-stream-comment-actions-container'}>
|
||||
<div className="commentActionsLeft comment__action-container">
|
||||
<div className="talk-embed-stream-comment-actions-container-left commentActionsLeft comment__action-container">
|
||||
<Slot
|
||||
fill="commentReactions"
|
||||
{...slotProps}
|
||||
@@ -545,7 +545,7 @@ export default class Comment extends React.Component {
|
||||
/>
|
||||
</ActionButton>}
|
||||
</div>
|
||||
<div className="commentActionsRight comment__action-container">
|
||||
<div className="talk-embed-stream-comment-actions-container-right commentActionsRight comment__action-container">
|
||||
<Slot
|
||||
fill="commentActions"
|
||||
wrapperComponent={ActionButton}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default class Embed extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {activeTab, commentId, root, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog} = this.props;
|
||||
const {activeTab, commentId, root, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog, router: {location: {query: {parentUrl}}}} = this.props;
|
||||
const {user} = this.props.auth;
|
||||
const hasHighlightedComment = !!commentId;
|
||||
|
||||
@@ -37,7 +37,7 @@ export default class Embed extends React.Component {
|
||||
<div className={cn('talk-embed-stream', {'talk-embed-stream-highlight-comment': hasHighlightedComment})}>
|
||||
<IfSlotIsNotEmpty slot="login">
|
||||
<Popup
|
||||
href='embed/stream/login'
|
||||
href={`embed/stream/login?parentUrl=${encodeURIComponent(parentUrl)}`}
|
||||
title='Login'
|
||||
features='menubar=0,resizable=0,width=500,height=550,top=200,left=500'
|
||||
open={showSignInDialog}
|
||||
|
||||
+3
-3
@@ -2,18 +2,18 @@ import React from 'react';
|
||||
|
||||
const CommentContent = ({comment}) => {
|
||||
const textbreaks = comment.body.split('\n');
|
||||
return <div className={`${name}-text`}>
|
||||
return <span className={`${name}-text`}>
|
||||
{
|
||||
textbreaks.map((line, i) => {
|
||||
return (
|
||||
<span key={i} className={`${name}-line`}>
|
||||
{line}
|
||||
<br className={`${name}-linebreak`}/>
|
||||
{i === textbreaks.length - 1 && <br className={`${name}-linebreak`}/>}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
</span>;
|
||||
};
|
||||
|
||||
export default CommentContent;
|
||||
@@ -71,7 +71,7 @@ class ProfileContainer extends Component {
|
||||
const emailAddress = localProfile && localProfile.id;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='talk-embed-stream-profile-container'>
|
||||
<h2>{user.username}</h2>
|
||||
{emailAddress ? <p>{emailAddress}</p> : null}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Icon} from '../coral-ui';
|
||||
import styles from './Comment.css';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentTimestamp from 'coral-framework/components/CommentTimestamp';
|
||||
import CommentContent from '../coral-embed-stream/src/components/CommentContent';
|
||||
import CommentContent from 'coral-framework/components/CommentContent';
|
||||
import cn from 'classnames';
|
||||
import {getTotalReactionsCount} from 'coral-framework/utils';
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './styles.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {BASE_PATH} from 'coral-framework/constants/url';
|
||||
|
||||
const ModerationLink = (props) => props.isAdmin ? (
|
||||
<div className={styles.moderationLink}>
|
||||
<a href={`${BASE_PATH}admin/moderate/${props.assetId}`} target="_blank">
|
||||
<div className={cn(styles.moderationLink, 'talk-embed-stream-moderation-container')}>
|
||||
<a className='talk-embed-stream-moderation-link' href={`${BASE_PATH}admin/moderate/${props.assetId}`} target="_blank">
|
||||
{t('moderate_this_stream')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -94,14 +94,17 @@ const CONFIG = {
|
||||
MONGO_URL: process.env.TALK_MONGO_URL,
|
||||
REDIS_URL: process.env.TALK_REDIS_URL,
|
||||
|
||||
// REDIS_RECONNECTION_MAX_ATTEMPTS is the amount of attempts that a redis
|
||||
// connection will attempt to reconnect before aborting with an error.
|
||||
REDIS_RECONNECTION_MAX_ATTEMPTS: parseInt(process.env.TALK_REDIS_RECONNECTION_MAX_ATTEMPTS || '100'),
|
||||
// REDIS_CLIENT_CONFIG is the optional configuration that is merged with the
|
||||
// function config to provide deep control of the redis connection beheviour.
|
||||
REDIS_CLIENT_CONFIG: process.env.TALK_REDIS_CLIENT_CONFIGURATION || '{}',
|
||||
|
||||
// REDIS_RECONNECTION_MAX_RETRY_TIME is the time in string format for the
|
||||
// maximum amount of time that a client can be considered "connecting" before
|
||||
// attempts at reconnection are aborted with an error.
|
||||
REDIS_RECONNECTION_MAX_RETRY_TIME: ms(process.env.TALK_REDIS_RECONNECTION_MAX_RETRY_TIME || '1 min'),
|
||||
// REDIS_CLUSTER_MODE allows configuration on the type of cluster mode enabled
|
||||
// on the redis client. Can be either `NONE` or `CLUSTER`.
|
||||
REDIS_CLUSTER_MODE: process.env.TALK_REDIS_CLUSTER_MODE || 'NONE',
|
||||
|
||||
// REDIS_CLUSTER_CONFIGURATION contains the json string for the redis cluster
|
||||
// configuration.
|
||||
REDIS_CLUSTER_CONFIGURATION: process.env.TALK_REDIS_CLUSTER_CONFIGURATION || '[]',
|
||||
|
||||
// REDIS_RECONNECTION_BACKOFF_FACTOR is the factor that will be multiplied
|
||||
// against the current attempt count inbetween attempts to connect to redis.
|
||||
@@ -246,6 +249,26 @@ if (process.env.NODE_ENV === 'test' && !CONFIG.REDIS_URL) {
|
||||
CONFIG.REDIS_URL = 'redis://localhost/1';
|
||||
}
|
||||
|
||||
// REDIS_CLUSTER_CONFIGURATION should be parsed when the cluster mode !== none.
|
||||
if (CONFIG.REDIS_CLUSTER_MODE === 'CLUSTER') {
|
||||
try {
|
||||
CONFIG.REDIS_CLUSTER_CONFIGURATION = JSON.parse(CONFIG.REDIS_CLUSTER_CONFIGURATION);
|
||||
} catch (err) {
|
||||
throw new Error('TALK_REDIS_CLUSTER_CONFIGURATION is not valid JSON, see https://github.com/luin/ioredis#cluster for valid syntax of the list of cluster nodes');
|
||||
}
|
||||
|
||||
if (!Array.isArray(CONFIG.REDIS_CLUSTER_CONFIGURATION)) {
|
||||
throw new Error('TALK_REDIS_CLUSTER_MODE is CLUSTER, but the TALK_REDIS_CLUSTER_CONFIGURATION is invalid, see https://github.com/luin/ioredis#cluster for valid syntax of the list of cluster nodes');
|
||||
}
|
||||
|
||||
if (CONFIG.REDIS_CLUSTER_CONFIGURATION.length === 0) {
|
||||
throw new Error('TALK_REDIS_CLUSTER_CONFIGURATION must have at least one node specified in the cluster, see https://github.com/luin/ioredis#cluster for valid syntax of the list of cluster nodes');
|
||||
}
|
||||
}
|
||||
|
||||
// Client config is a JSON encoded string, defaulting to `{}`.
|
||||
CONFIG.REDIS_CLIENT_CONFIG = JSON.parse(CONFIG.REDIS_CLIENT_CONFIG);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Recaptcha configuration
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -55,18 +55,21 @@ These are only used during the webpack build.
|
||||
#### Advanced
|
||||
{:.no_toc}
|
||||
|
||||
- `TALK_REDIS_RECONNECTION_MAX_ATTEMPTS` (_optional_) - the amount of attempts
|
||||
that a redis connection will attempt to reconnect before aborting with an
|
||||
error. (Default `100`)
|
||||
- `TALK_REDIS_RECONNECTION_MAX_RETRY_TIME` (_optional_) - the time in string
|
||||
format for the maximum amount of time that a client can be considered
|
||||
"connecting" before attempts at reconnection are aborted with an error.
|
||||
(Default `1 min`)
|
||||
- `TALK_REDIS_CLIENT_CONFIG` (_optional_) - configuration overrides for the
|
||||
redis client configuration in a JSON encoded string. Configuration is
|
||||
overridden as the second parameter to the redis client constructor, and is
|
||||
merged with default configuration. (Default `{}`)
|
||||
- `TALK_REDIS_RECONNECTION_BACKOFF_FACTOR` (_optional_) - the time factor that
|
||||
will be multiplied against the current attempt count inbetween attempts to
|
||||
connect to redis. (Default `500 ms`)
|
||||
- `TALK_REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME` (_optional_) - the minimum time
|
||||
used to delay before attempting to reconnect to redis. (Default `1 sec`)
|
||||
- `TALK_REDIS_CLUSTER_MODE` (_optional_) - the cluster mode of the redis client.
|
||||
Can be either `NONE` or `CLUSTER`. (Default `NONE`)
|
||||
- `TALK_REDIS_CLUSTER_CONFIGURATION` (_optional_) - the json serialized form of
|
||||
the cluster nodes. Only required when `TALK_REDIS_CLUSTER_MODE=CLUSTER`. See
|
||||
https://github.com/luin/ioredis#cluster for configuration details.
|
||||
(Default `[]`)
|
||||
|
||||
### Server
|
||||
|
||||
@@ -137,7 +140,7 @@ is not needed in most situations.
|
||||
use to set a cookie containing a JWT that was issued by Talk.
|
||||
(Default `process.env.TALK_JWT_COOKIE_NAME`)
|
||||
- `TALK_JWT_COOKIE_NAMES` (_optional_) - the different cookie names to check for
|
||||
a JWT token in, seperated by `,`. By default, we always use the
|
||||
a JWT token in, separated by `,`. By default, we always use the
|
||||
`process.env.TALK_JWT_COOKIE_NAME` and `process.env.TALK_JWT_SIGNING_COOKIE_NAME`
|
||||
for this value. Any additional cookie names specified here will be appended to
|
||||
the list of cookie names to inspect.
|
||||
|
||||
@@ -265,6 +265,7 @@ pt_BR:
|
||||
mod_faster: "Moderado mais rápido com atalhos de teclado"
|
||||
moderate: "Moderar →"
|
||||
more_detail: "Mais detalhes"
|
||||
new: "Novo"
|
||||
newest_first: "Mais novo primeiro"
|
||||
navigation: Navegação
|
||||
next_comment: "Vá para o próximo comentário"
|
||||
|
||||
+3
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "3.5.1",
|
||||
"version": "3.6.0",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"main": "app.js",
|
||||
"private": true,
|
||||
@@ -103,6 +103,7 @@
|
||||
"fs-extra": "^4.0.1",
|
||||
"gql-merge": "^0.0.4",
|
||||
"graphql": "^0.9.1",
|
||||
"graphql-anywhere": "^3.1.0",
|
||||
"graphql-docs": "^0.2.0",
|
||||
"graphql-errors": "^2.1.0",
|
||||
"graphql-redis-subscriptions": "^1.3.0",
|
||||
@@ -144,7 +145,6 @@
|
||||
"passport-local": "^1.0.0",
|
||||
"pluralize": "^7.0.0",
|
||||
"postcss-loader": "^1.3.3",
|
||||
"postcss-modules": "^0.5.2",
|
||||
"postcss-smart-import": "^0.5.1",
|
||||
"precss": "^1.4.0",
|
||||
"prop-types": "^15.5.10",
|
||||
@@ -180,7 +180,6 @@
|
||||
"url-search-params": "^0.9.0",
|
||||
"uuid": "^3.1.0",
|
||||
"webpack": "^2.3.1",
|
||||
"webpack-sources": "^1.0.1",
|
||||
"yaml-loader": "^0.4.0",
|
||||
"yamljs": "^0.2.10"
|
||||
},
|
||||
@@ -197,8 +196,7 @@
|
||||
"nodemon": "^1.11.0",
|
||||
"pre-git": "^3.15.3",
|
||||
"sinon": "^3.2.1",
|
||||
"sinon-chai": "^2.13.0",
|
||||
"supertest": "^2.0.1"
|
||||
"sinon-chai": "^2.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^8"
|
||||
|
||||
@@ -4,3 +4,4 @@ export {default as IfSlotIsEmpty} from 'coral-framework/components/IfSlotIsEmpty
|
||||
export {default as IfSlotIsNotEmpty} from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
export {default as CommentAuthorName} from 'coral-framework/components/CommentAuthorName';
|
||||
export {default as CommentTimestamp} from 'coral-framework/components/CommentTimestamp';
|
||||
export {default as CommentContent} from 'coral-framework/components/CommentContent';
|
||||
|
||||
@@ -5,20 +5,20 @@ const name = 'talk-plugin-comment-content';
|
||||
|
||||
const CommentContent = ({comment}) => {
|
||||
const textbreaks = comment.body.split('\n');
|
||||
return <div className={`${name}-text`}>
|
||||
return <span className={`${name}-text`}>
|
||||
{
|
||||
textbreaks.map((line, i) => {
|
||||
return (
|
||||
<span key={i} className={`${name}-line`}>
|
||||
<Linkify properties={{target: '_blank'}}>
|
||||
{line}
|
||||
{line.trim()}
|
||||
</Linkify>
|
||||
<br className={`${name}-linebreak`}/>
|
||||
{i !== textbreaks.length - 1 && <br className={`${name}-linebreak`}/>}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
</span>;
|
||||
};
|
||||
|
||||
export default CommentContent;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './Comment.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Slot, CommentAuthorName, CommentTimestamp} from 'plugin-api/beta/client/components';
|
||||
import {Slot, CommentAuthorName, CommentTimestamp, CommentContent} from 'plugin-api/beta/client/components';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
import {pluginName} from '../../package.json';
|
||||
import FeaturedButton from '../containers/FeaturedButton';
|
||||
@@ -19,9 +19,14 @@ class Comment extends React.Component {
|
||||
return (
|
||||
<div className={cn(styles.root, `${pluginName}-comment`)}>
|
||||
|
||||
<blockquote className={cn(styles.quote, `${pluginName}-comment-body`)}>
|
||||
{comment.body}
|
||||
</blockquote>
|
||||
<Slot
|
||||
component={'blockquote'}
|
||||
className={cn(styles.quote, `${pluginName}-comment-body`)}
|
||||
fill="commentContent"
|
||||
defaultComponent={CommentContent}
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>
|
||||
|
||||
<div className={cn(`${pluginName}-comment-username-box`)}>
|
||||
|
||||
@@ -46,7 +51,7 @@ class Comment extends React.Component {
|
||||
</div>
|
||||
|
||||
<footer className={cn(styles.footer, `${pluginName}-comment-footer`)}>
|
||||
<div className={cn(styles.reactionsContainer, `${pluginName}-comment-reactions`)}>
|
||||
<div className={cn('talk-embed-stream-comment-actions-container-left', styles.reactionsContainer, `${pluginName}-comment-reactions`)}>
|
||||
|
||||
<Slot
|
||||
fill="commentReactions"
|
||||
@@ -64,7 +69,7 @@ class Comment extends React.Component {
|
||||
asset={asset}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(styles.actionsContainer, `${pluginName}-comment-actions`)}>
|
||||
<div className={cn('talk-embed-stream-comment-actions-container-right', styles.actionsContainer, `${pluginName}-comment-actions`)}>
|
||||
<button className={cn(styles.goTo, `${pluginName}-comment-go-to`)} onClick={this.viewComment}>
|
||||
<Icon name="forum" className={styles.repliesIcon} /> {comment.replyCount} | {t('talk-plugin-featured-comments.go_to_conversation')} <Icon name="keyboard_arrow_right" className={styles.goToIcon} />
|
||||
</button>
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class Tag extends React.Component {
|
||||
<span className={cn(styles.tagContainer, styles.noSelect)} onMouseEnter={this.showTooltip}
|
||||
onMouseLeave={this.hideTooltip} onTouchStart={this.showTooltip}
|
||||
onTouchEnd={this.hideTooltip} >
|
||||
<span className={cn(styles.tag, styles.noSelect, {[styles.on]: tooltip})}>
|
||||
<span className={cn(styles.tag, styles.noSelect, {[styles.on]: tooltip}, 'talk-stream-comment-featured-tag-label')}>
|
||||
{t('talk-plugin-featured-comments.featured')}
|
||||
</span>
|
||||
{tooltip && <Tooltip className={styles.tooltip} />}
|
||||
|
||||
@@ -35,7 +35,7 @@ class LikeButton extends React.Component {
|
||||
return (
|
||||
<div className={cn(styles.container, `${plugin}-container`)}>
|
||||
<button
|
||||
className={cn(styles.button, {[styles.liked]: alreadyReacted}, `${plugin}-button`)}
|
||||
className={cn(styles.button, {[`${styles.liked} talk-plugin-like-liked`]: alreadyReacted}, `${plugin}-button`)}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span className={cn(`${plugin}-label`, styles.label)}>
|
||||
|
||||
@@ -2,12 +2,13 @@ import React from 'react';
|
||||
import styles from './OffTopicTag.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {isTagged} from 'plugin-api/beta/client/utils';
|
||||
import cn from 'classnames';
|
||||
|
||||
export default (props) => (
|
||||
<span>
|
||||
{
|
||||
isTagged(props.comment.tags, 'OFF_TOPIC') && props.depth === 0 ? (
|
||||
<span className={styles.tag}>
|
||||
<span className={cn(styles.tag, 'talk-stream-comment-offtopic-tag-label')}>
|
||||
{t('off_topic')}
|
||||
</span>
|
||||
) : null
|
||||
|
||||
@@ -73,7 +73,9 @@ export default class PermalinkButton extends React.Component {
|
||||
ref={(ref) => this.linkButton = ref}
|
||||
onClick={this.toggle}
|
||||
className={cn(`${name}-button`, styles.button)}>
|
||||
{t('permalink')}
|
||||
<span className='talk-plugin-permalink-button-label'>
|
||||
{t('permalink')}
|
||||
</span>
|
||||
<Icon name="link" className={styles.icon}/>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class RespectButton extends React.Component {
|
||||
return (
|
||||
<div className={cn(styles.container, `${plugin}-container`)}>
|
||||
<button
|
||||
className={cn(styles.button, {[styles.respected]: alreadyReacted}, `${plugin}-button`)}
|
||||
className={cn(styles.button, {[`${styles.respected} talk-plugin-respect-respected`]: alreadyReacted}, `${plugin}-button`)}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span className={cn(`${plugin}-label`, styles.label)}>
|
||||
|
||||
+16
-23
@@ -4,9 +4,6 @@ const UsersService = require('../../../services/users');
|
||||
const mailer = require('../../../services/mailer');
|
||||
const authorization = require('../../../middleware/authorization');
|
||||
const errors = require('../../../errors');
|
||||
const {
|
||||
ROOT_URL
|
||||
} = require('../../../config');
|
||||
|
||||
//==============================================================================
|
||||
// ROUTES
|
||||
@@ -50,22 +47,17 @@ router.post('/password/reset', async (req, res, next) => {
|
||||
|
||||
try {
|
||||
let token = await UsersService.createPasswordResetToken(email, loc);
|
||||
if (!token) {
|
||||
res.status(204).end();
|
||||
return;
|
||||
if (token) {
|
||||
await mailer.sendSimple({
|
||||
template: 'password-reset',
|
||||
locals: {
|
||||
token,
|
||||
},
|
||||
subject: 'Password Reset',
|
||||
to: email
|
||||
});
|
||||
}
|
||||
|
||||
// Send the password reset email.
|
||||
await mailer.sendSimple({
|
||||
template: 'password-reset', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
token,
|
||||
rootURL: ROOT_URL
|
||||
},
|
||||
subject: 'Password Reset',
|
||||
to: email
|
||||
});
|
||||
|
||||
res.status(204).end();
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -78,22 +70,23 @@ router.post('/password/reset', async (req, res, next) => {
|
||||
* 2) the new password {String}
|
||||
*/
|
||||
router.put('/password/reset', async (req, res, next) => {
|
||||
|
||||
const {
|
||||
token,
|
||||
password
|
||||
} = req.body;
|
||||
const {check} = req.query;
|
||||
const {token, password} = req.body;
|
||||
|
||||
if (!token) {
|
||||
return next(errors.ErrMissingToken);
|
||||
}
|
||||
|
||||
if (!password || password.length < 8) {
|
||||
if (check !== 'true' && (!password || password.length < 8)) {
|
||||
return next(errors.ErrPasswordTooShort);
|
||||
}
|
||||
|
||||
try {
|
||||
let [user, loc] = await UsersService.verifyPasswordResetToken(token);
|
||||
if (check === 'true') {
|
||||
res.status(204).end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Change the users' password.
|
||||
await UsersService.changePassword(user.id, password);
|
||||
|
||||
+26
-27
@@ -2,6 +2,8 @@ const debug = require('debug')('talk:services:domainlist');
|
||||
const _ = require('lodash');
|
||||
const SettingsService = require('./settings');
|
||||
|
||||
const {ROOT_URL} = require('../config');
|
||||
|
||||
/**
|
||||
* The root domainlist object.
|
||||
* @type {Object}
|
||||
@@ -17,31 +19,24 @@ class Domainlist {
|
||||
/**
|
||||
* Loads domains white list in from the database
|
||||
*/
|
||||
load() {
|
||||
return SettingsService
|
||||
.retrieve()
|
||||
.then((settings) => {
|
||||
|
||||
// Insert the settings domains whitelist.
|
||||
this.upsert(settings.domains);
|
||||
});
|
||||
async load() {
|
||||
const {domains} = await SettingsService.retrieve();
|
||||
this.upsert(domains);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the domains whitelist data
|
||||
* @param {Array} list list of domains to be set to the whitelist
|
||||
*/
|
||||
upsert(lists) {
|
||||
async upsert(lists) {
|
||||
|
||||
// Add the domains to this array and also be sure are all unique domains
|
||||
if (!('whitelist' in lists)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lists['whitelist'] = Domainlist.parseList(lists['whitelist']);
|
||||
debug(`Added ${lists['whitelist'].length} domains to the whitelist.`);
|
||||
|
||||
return Promise.resolve(this);
|
||||
this.lists.whitelist = Domainlist.parseList(lists.whitelist);
|
||||
debug(`Added ${lists.whitelist.length} domains to the whitelist.`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,19 +46,22 @@ class Domainlist {
|
||||
*/
|
||||
match(list, url) {
|
||||
|
||||
// Parse the url that we're matching with.
|
||||
const domainToMatch = Domainlist.parseURL(url);
|
||||
|
||||
// This will return true in the event that at least one blockword is found
|
||||
// in the phrase.
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i] === domainToMatch) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return list.indexOf(domainToMatch) >= 0;
|
||||
}
|
||||
|
||||
// We've walked over all the whitelisted domains, and haven't had a
|
||||
// mismatch... It is not an allowed domain!
|
||||
return false;
|
||||
/**
|
||||
* Checks to see if the passed url matches the domain of the root path.
|
||||
*
|
||||
* @param {String} url
|
||||
* @returns {Boolean} true if the domains match
|
||||
*/
|
||||
static matchMount(url) {
|
||||
return Domainlist.parseURL(url) === Domainlist.parseURL(ROOT_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +82,7 @@ class Domainlist {
|
||||
let domain;
|
||||
|
||||
// removes protocol and get domain
|
||||
if (url.indexOf('://') > -1) {
|
||||
if (url.indexOf('//') > -1) {
|
||||
domain = url.split('/')[2];
|
||||
} else {
|
||||
domain = url.split('/')[0];
|
||||
@@ -96,13 +94,14 @@ class Domainlist {
|
||||
return domain.toLowerCase();
|
||||
}
|
||||
|
||||
static urlCheck(url) {
|
||||
static async urlCheck(url) {
|
||||
const dl = new Domainlist();
|
||||
|
||||
return dl.load()
|
||||
.then(() => {
|
||||
return dl.match(dl.lists['whitelist'], url);
|
||||
});
|
||||
// Load the domain list.
|
||||
await dl.load();
|
||||
|
||||
// Perform a match.
|
||||
return dl.match(dl.lists.whitelist, url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<p><%= t('email.confirm.has_been_requested') %> <b><%= email %></b>.</p>
|
||||
<p><%= t('email.confirm.to_confirm') %> <a href="<%= rootURL %>/admin/confirm-email#<%= token %>">Confirm Email</a></p>
|
||||
<p><%= t('email.confirm.to_confirm') %> <a href="<%= BASE_URL %>admin/confirm-email#<%= token %>">Confirm Email</a></p>
|
||||
<p><%= t('email.confirm.if_you_did_not') %></p>
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
<%= t('email.confirm.to_confirm') %>
|
||||
|
||||
<%= rootURL %>/confirm/endpoint#<%= token %>
|
||||
<%= BASE_URL %>confirm/endpoint#<%= token %>
|
||||
|
||||
<%= t('email.confirm.if_you_did_not') %>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<p><%= t('email.password_reset.we_received_a_request') %><br />
|
||||
<%= t('email.password_reset.if_you_did') %> <a href="<%= rootURL %>/admin/password-reset#<%= token %>"><%= t('email.password_reset.please_click') %></a>.</p>
|
||||
<%= t('email.password_reset.if_you_did') %> <a href="<%= BASE_URL %>admin/password-reset#<%= token %>"><%= t('email.password_reset.please_click') %></a>.</p>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<%= t('email.password_reset.we_received_a_request') %>. <%= t('email.password_reset.if_you_did') %> <%= t('email.password_reset.please_click') %>:
|
||||
|
||||
<%= rootURL %>/admin/password-reset#<%= token %>
|
||||
<%= BASE_URL %>admin/password-reset#<%= token %>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const {
|
||||
BASE_URL,
|
||||
BASE_PATH,
|
||||
MOUNT_PATH,
|
||||
STATIC_URL,
|
||||
} = require('../url');
|
||||
|
||||
const applyLocals = (locals) => {
|
||||
|
||||
// Apply the BASE_PATH, BASE_URL, and MOUNT_PATH on the app.locals, which will
|
||||
// make them available on the templates and the routers.
|
||||
locals.BASE_URL = BASE_URL;
|
||||
locals.BASE_PATH = BASE_PATH;
|
||||
locals.MOUNT_PATH = MOUNT_PATH;
|
||||
locals.STATIC_URL = STATIC_URL;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
applyLocals,
|
||||
};
|
||||
@@ -4,6 +4,7 @@ const kue = require('./kue');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const _ = require('lodash');
|
||||
const {applyLocals} = require('./locals');
|
||||
|
||||
const i18n = require('./i18n');
|
||||
|
||||
@@ -92,6 +93,9 @@ const mailer = module.exports = {
|
||||
// Prefix the subject with `[Talk]`.
|
||||
subject = `[Talk] ${subject}`;
|
||||
|
||||
applyLocals(locals);
|
||||
|
||||
// Attach the templating function.
|
||||
locals['t'] = i18n.t;
|
||||
|
||||
return Promise.all([
|
||||
|
||||
+23
-35
@@ -1,12 +1,14 @@
|
||||
const Redis = require('ioredis');
|
||||
const merge = require('lodash/merge');
|
||||
const debug = require('debug')('talk:services:redis');
|
||||
const enabled = require('debug').enabled('talk:services:redis');
|
||||
const {
|
||||
REDIS_URL,
|
||||
REDIS_RECONNECTION_MAX_ATTEMPTS,
|
||||
REDIS_RECONNECTION_MAX_RETRY_TIME,
|
||||
REDIS_RECONNECTION_BACKOFF_FACTOR,
|
||||
REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME,
|
||||
REDIS_CLIENT_CONFIG,
|
||||
REDIS_CLUSTER_MODE,
|
||||
REDIS_CLUSTER_CONFIGURATION,
|
||||
} = require('../config');
|
||||
|
||||
const attachMonitors = (client) => {
|
||||
@@ -16,8 +18,8 @@ const attachMonitors = (client) => {
|
||||
if (enabled) {
|
||||
client.on('connect', () => debug('client connected'));
|
||||
client.on('ready', () => debug('client ready'));
|
||||
client.on('reconnecting', () => debug('client connection lost, attempting to reconnect'));
|
||||
client.on('close', () => debug('client closed the connection'));
|
||||
client.on('reconnecting', () => debug('client connection lost, attempting to reconnect'));
|
||||
client.on('end', () => debug('client ended'));
|
||||
}
|
||||
|
||||
@@ -27,44 +29,30 @@ const attachMonitors = (client) => {
|
||||
console.error('Error connecting to redis:', err);
|
||||
}
|
||||
});
|
||||
client.on('node error', (err) => debug('node error', err));
|
||||
};
|
||||
|
||||
const connectionOptions = {
|
||||
retry_strategy: function(options) {
|
||||
if (options.error && options.error.code !== 'ECONNREFUSED') {
|
||||
function retryStrategy(times) {
|
||||
const delay = Math.max(times * REDIS_RECONNECTION_BACKOFF_FACTOR, REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME);
|
||||
|
||||
debug('retry strategy: none, an error occured');
|
||||
debug(`retry strategy: try to reconnect ${delay} ms from now`);
|
||||
|
||||
// End reconnecting on a specific error and flush all commands with a individual error
|
||||
return options.error;
|
||||
}
|
||||
if (options.total_retry_time > REDIS_RECONNECTION_MAX_RETRY_TIME) {
|
||||
|
||||
debug('retry strategy: none, exhausted retry time');
|
||||
|
||||
// End reconnecting after a specific timeout and flush all commands with a individual error
|
||||
return new Error('Retry time exhausted');
|
||||
}
|
||||
|
||||
if (options.attempt > REDIS_RECONNECTION_MAX_ATTEMPTS) {
|
||||
|
||||
debug('retry strategy: none, exhausted retry attempts');
|
||||
|
||||
// End reconnecting with built in error
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// reconnect after
|
||||
const delay = Math.max(options.attempt * REDIS_RECONNECTION_BACKOFF_FACTOR, REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME);
|
||||
|
||||
debug(`retry strategy: try to reconnect ${delay} ms from now`);
|
||||
|
||||
return delay;
|
||||
}
|
||||
};
|
||||
return delay;
|
||||
}
|
||||
|
||||
const createClient = () => {
|
||||
let client = new Redis(REDIS_URL, connectionOptions);
|
||||
let client;
|
||||
if (REDIS_CLUSTER_MODE === 'NONE') {
|
||||
client = new Redis(REDIS_URL, merge({}, REDIS_CLIENT_CONFIG, {
|
||||
retryStrategy,
|
||||
}));
|
||||
} else if (REDIS_CLUSTER_MODE === 'CLUSTER') {
|
||||
client = new Redis.Cluster(REDIS_CLUSTER_CONFIGURATION, merge({
|
||||
scaleReads: 'slave',
|
||||
}, REDIS_CLIENT_CONFIG, {
|
||||
clusterRetryStrategy: retryStrategy,
|
||||
}));
|
||||
}
|
||||
|
||||
// Attach the monitors that will print debug messages to the console.
|
||||
attachMonitors(client);
|
||||
|
||||
+19
-27
@@ -1,8 +1,6 @@
|
||||
const assert = require('assert');
|
||||
const uuid = require('uuid');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const url = require('url');
|
||||
const Wordlist = require('./wordlist');
|
||||
const errors = require('../errors');
|
||||
|
||||
const {
|
||||
@@ -22,9 +20,10 @@ const USER_ROLES = require('../models/enum/user_roles');
|
||||
const RECAPTCHA_WINDOW_SECONDS = 60 * 10; // 10 minutes.
|
||||
const RECAPTCHA_INCORRECT_TRIGGER = 5; // after 3 incorrect attempts, recaptcha will be required.
|
||||
|
||||
const SettingsService = require('./settings');
|
||||
const ActionsService = require('./actions');
|
||||
const MailerService = require('./mailer');
|
||||
const Wordlist = require('./wordlist');
|
||||
const Domainlist = require('./domainlist');
|
||||
|
||||
const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm';
|
||||
const PASSWORD_RESET_JWT_SUBJECT = 'password_reset';
|
||||
@@ -557,11 +556,10 @@ module.exports = class UsersService {
|
||||
|
||||
email = email.toLowerCase();
|
||||
|
||||
const [user, settings] = await Promise.all([
|
||||
const [user, domainValidated] = await Promise.all([
|
||||
UserModel.findOne({profiles: {$elemMatch: {id: email}}}),
|
||||
SettingsService.retrieve(),
|
||||
Domainlist.urlCheck(loc),
|
||||
]);
|
||||
|
||||
if (!user) {
|
||||
|
||||
// Since we don't want to reveal that the email does/doesn't exist
|
||||
@@ -569,19 +567,11 @@ module.exports = class UsersService {
|
||||
// endpoint.
|
||||
return;
|
||||
}
|
||||
let redirectDomain;
|
||||
try {
|
||||
const {hostname, port} = url.parse(loc);
|
||||
redirectDomain = hostname;
|
||||
if (port) {
|
||||
redirectDomain += `:${port}`;
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error('redirect location is invalid');
|
||||
}
|
||||
|
||||
if (settings.domains.whitelist.indexOf(redirectDomain) === -1) {
|
||||
throw new Error('redirect location is not on the list of acceptable domains');
|
||||
// If the domain didn't match any of the whitelisted domains and if it
|
||||
// didn't match the mount domain, then throw an error.
|
||||
if (!domainValidated && !Domainlist.matchMount(loc)) {
|
||||
throw new Error('user supplied location exists on non-permitted domain');
|
||||
}
|
||||
|
||||
const payload = {
|
||||
@@ -619,16 +609,18 @@ module.exports = class UsersService {
|
||||
* Verifies a jwt and returns the associated user.
|
||||
* @param {String} token the JSON Web Token to verify
|
||||
*/
|
||||
static verifyPasswordResetToken(token) {
|
||||
return UsersService
|
||||
.verifyToken(token, {
|
||||
subject: PASSWORD_RESET_JWT_SUBJECT
|
||||
})
|
||||
static async verifyPasswordResetToken(token) {
|
||||
const {userId, loc, version} = await UsersService.verifyToken(token, {
|
||||
subject: PASSWORD_RESET_JWT_SUBJECT
|
||||
});
|
||||
|
||||
// TODO: add search by __v as well
|
||||
.then((decoded) => {
|
||||
return Promise.all([UsersService.findById(decoded.userId), decoded.loc]);
|
||||
});
|
||||
const user = await UsersService.findById(userId);
|
||||
|
||||
if (version !== user.__v) {
|
||||
throw new Error('password reset token has expired');
|
||||
}
|
||||
|
||||
return [user, loc];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,13 +26,84 @@ describe('services.Domainlist', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('#parseURL', () => {
|
||||
it('parses the domain correctly', () => {
|
||||
[
|
||||
['http://google.ca/test', 'google.ca'],
|
||||
['http://google.ca:80/test', 'google.ca'],
|
||||
['https://google.ca/test', 'google.ca'],
|
||||
['https://google.ca:443/test', 'google.ca'],
|
||||
['//google.ca/test', 'google.ca'],
|
||||
['//google.ca:80/test', 'google.ca'],
|
||||
['//google.ca:443/test', 'google.ca'],
|
||||
['google.ca/test', 'google.ca'],
|
||||
['google.ca:80/test', 'google.ca'],
|
||||
['google.ca:443/test', 'google.ca'],
|
||||
['http://google.ca/', 'google.ca'],
|
||||
['http://google.ca:80/', 'google.ca'],
|
||||
['https://google.ca/', 'google.ca'],
|
||||
['https://google.ca:443/', 'google.ca'],
|
||||
['//google.ca/', 'google.ca'],
|
||||
['//google.ca:80/', 'google.ca'],
|
||||
['//google.ca:443/', 'google.ca'],
|
||||
['google.ca/', 'google.ca'],
|
||||
['google.ca:80/', 'google.ca'],
|
||||
['google.ca:443/', 'google.ca'],
|
||||
['google.ca', 'google.ca'],
|
||||
['http://google.ca', 'google.ca'],
|
||||
['http://google.ca:80', 'google.ca'],
|
||||
['https://google.ca', 'google.ca'],
|
||||
['https://google.ca:443', 'google.ca'],
|
||||
['//google.ca', 'google.ca'],
|
||||
['//google.ca:80', 'google.ca'],
|
||||
['//google.ca:443', 'google.ca'],
|
||||
['google.ca', 'google.ca'],
|
||||
['google.ca:80', 'google.ca'],
|
||||
['google.ca:443', 'google.ca'],
|
||||
['http://google.Ca/test', 'google.ca'],
|
||||
['http://google.ca:80/test', 'google.ca'],
|
||||
['https://google.Ca/test', 'google.ca'],
|
||||
['https://google.ca:443/test', 'google.ca'],
|
||||
['//google.Ca/test', 'google.ca'],
|
||||
['//google.Ca:80/test', 'google.ca'],
|
||||
['//google.Ca:443/test', 'google.ca'],
|
||||
['google.Ca/test', 'google.ca'],
|
||||
['google.ca:80/test', 'google.ca'],
|
||||
['google.ca:443/test', 'google.ca'],
|
||||
['http://Google.ca/', 'google.ca'],
|
||||
['http://google.Ca:80/', 'google.ca'],
|
||||
['https://Google.ca/', 'google.ca'],
|
||||
['https://google.Ca:443/', 'google.ca'],
|
||||
['//Google.ca/', 'google.ca'],
|
||||
['//google.Ca:80/', 'google.ca'],
|
||||
['//google.Ca:443/', 'google.ca'],
|
||||
['Google.ca/', 'google.ca'],
|
||||
['google.Ca:80/', 'google.ca'],
|
||||
['google.Ca:443/', 'google.ca'],
|
||||
['Google.ca', 'google.ca'],
|
||||
['http://Google.ca', 'google.ca'],
|
||||
['http://google.Ca:80', 'google.ca'],
|
||||
['https://Google.ca', 'google.ca'],
|
||||
['https://google.Ca:443', 'google.ca'],
|
||||
['//Google.ca', 'google.ca'],
|
||||
['//google.Ca:80', 'google.ca'],
|
||||
['//google.Ca:443', 'google.ca'],
|
||||
['Google.ca', 'google.ca'],
|
||||
['google.Ca:80', 'google.ca'],
|
||||
['google.Ca:443', 'google.ca'],
|
||||
].forEach(([domain, hostname]) => {
|
||||
expect(Domainlist.parseURL(domain), `domain ${domain} should be parsed as ${hostname}`).to.equal(hostname);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#match', () => {
|
||||
|
||||
const whiteList = Domainlist.parseList(domainlists['whitelist']);
|
||||
|
||||
it('does match on an included domain', () => {
|
||||
[
|
||||
'wapo.com',
|
||||
'http://wapo.com',
|
||||
'nytimes.com'
|
||||
].forEach((domain) => {
|
||||
expect(domainlist.match(whiteList, domain)).to.be.true;
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#root form {
|
||||
.container {
|
||||
max-width: 300px;
|
||||
border: 1px solid lightgrey;
|
||||
box-shadow: 0px 10px 24px 2px rgba(0,0,0,0.2);
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
#root form {
|
||||
display: none;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
@@ -81,7 +83,8 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<form id="reset-password-form">
|
||||
<div class="error-console container"></div>
|
||||
<form id="reset-password-form" class="container">
|
||||
<legend class="legend">Set new password</legend>
|
||||
<label for="password">
|
||||
New password
|
||||
@@ -94,14 +97,18 @@
|
||||
<input type="password" name="confirm-password" placeholder="confirm password" />
|
||||
</label>
|
||||
<button class="submit-password-reset" type="submit">Apply</button>
|
||||
<div class="error-console">foo</div>
|
||||
</form>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
function showError(message) {
|
||||
$('.error-console').text(message).addClass('active');
|
||||
function showError(error) {
|
||||
try {
|
||||
var err = JSON.parse(error);
|
||||
$('.error-console').text(err.message).addClass('active');
|
||||
} catch (err) {
|
||||
$('.error-console').text(error).addClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit (e) {
|
||||
@@ -111,8 +118,13 @@
|
||||
var password = $('[name="password"]').val();
|
||||
var confirm = $('[name="confirm-password"]').val();
|
||||
|
||||
if (password !== confirm || password === '' || password.length < 8) {
|
||||
showError('passwords must match and be 8 characters.');
|
||||
if (password === '' || password.length < 8) {
|
||||
showError('Passwords must be at least 8 characters.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== confirm) {
|
||||
showError('New password and confirm password must match');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -128,7 +140,17 @@
|
||||
});
|
||||
}
|
||||
|
||||
$('#reset-password-form').on('submit', handleSubmit);
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/password/reset?check=true',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({token: location.hash.replace('#', '')})
|
||||
}).then(function () {
|
||||
$('#reset-password-form').fadeIn().on('submit', handleSubmit);
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -1878,17 +1878,6 @@ css-loader@^0.28.5:
|
||||
postcss-value-parser "^3.3.0"
|
||||
source-list-map "^2.0.0"
|
||||
|
||||
css-modules-loader-core@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/css-modules-loader-core/-/css-modules-loader-core-1.0.1.tgz#94e3eec9bc8174df0f974641f3e0d0550497f694"
|
||||
dependencies:
|
||||
icss-replace-symbols "1.0.2"
|
||||
postcss "5.1.2"
|
||||
postcss-modules-extract-imports "1.0.0"
|
||||
postcss-modules-local-by-default "1.1.1"
|
||||
postcss-modules-scope "1.0.2"
|
||||
postcss-modules-values "1.2.2"
|
||||
|
||||
css-parse@1.7.x:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-1.7.0.tgz#321f6cf73782a6ff751111390fc05e2c657d8c9b"
|
||||
@@ -2974,12 +2963,6 @@ gauge@~2.7.1:
|
||||
strip-ansi "^3.0.1"
|
||||
wide-align "^1.1.0"
|
||||
|
||||
generic-names@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-1.0.2.tgz#e25b7feceb5b5a8f28f5f972a7ccfe57e562adcd"
|
||||
dependencies:
|
||||
loader-utils "^0.2.16"
|
||||
|
||||
get-caller-file@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
|
||||
@@ -3201,6 +3184,10 @@ graphql-anywhere@^3.0.0, graphql-anywhere@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-3.0.1.tgz#73531db861174c8f212eafb9f8e84944b38b4e5a"
|
||||
|
||||
graphql-anywhere@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-3.1.0.tgz#3ea0d8e8646b5cee68035016a9a7557c15c21e96"
|
||||
|
||||
graphql-docs@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql-docs/-/graphql-docs-0.2.0.tgz#cf803f9c9d354fa03e89073d74e419261a5bfa74"
|
||||
@@ -3527,7 +3514,7 @@ iconv-lite@^0.4.17, iconv-lite@~0.4.13:
|
||||
version "0.4.18"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
|
||||
|
||||
icss-replace-symbols@1.0.2, icss-replace-symbols@^1.0.2:
|
||||
icss-replace-symbols@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.0.2.tgz#cb0b6054eb3af6edc9ab1d62d01933e2d4c8bfa5"
|
||||
|
||||
@@ -4762,7 +4749,7 @@ metascraper@^1.0.7:
|
||||
popsicle "^6.2.0"
|
||||
to-title-case "^1.0.0"
|
||||
|
||||
methods@1.x, methods@^1.1.1, methods@^1.1.2, methods@~1.1.2:
|
||||
methods@^1.1.1, methods@^1.1.2, methods@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||
|
||||
@@ -5798,48 +5785,33 @@ postcss-mixins@^2.1.0:
|
||||
postcss "^5.0.10"
|
||||
postcss-simple-vars "^1.0.1"
|
||||
|
||||
postcss-modules-extract-imports@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.0.0.tgz#5b07f368e350cda6fd5c8844b79123a7bd3e37be"
|
||||
dependencies:
|
||||
postcss "^5.0.4"
|
||||
|
||||
postcss-modules-extract-imports@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.0.1.tgz#8fb3fef9a6dd0420d3f6d4353cf1ff73f2b2a341"
|
||||
dependencies:
|
||||
postcss "^5.0.4"
|
||||
|
||||
postcss-modules-local-by-default@1.1.1, postcss-modules-local-by-default@^1.0.1:
|
||||
postcss-modules-local-by-default@^1.0.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.1.1.tgz#29a10673fa37d19251265ca2ba3150d9040eb4ce"
|
||||
dependencies:
|
||||
css-selector-tokenizer "^0.6.0"
|
||||
postcss "^5.0.4"
|
||||
|
||||
postcss-modules-scope@1.0.2, postcss-modules-scope@^1.0.0:
|
||||
postcss-modules-scope@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.0.2.tgz#ff977395e5e06202d7362290b88b1e8cd049de29"
|
||||
dependencies:
|
||||
css-selector-tokenizer "^0.6.0"
|
||||
postcss "^5.0.4"
|
||||
|
||||
postcss-modules-values@1.2.2, postcss-modules-values@^1.1.0:
|
||||
postcss-modules-values@^1.1.0:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.2.2.tgz#f0e7d476fe1ed88c5e4c7f97533a3e772ad94ca1"
|
||||
dependencies:
|
||||
icss-replace-symbols "^1.0.2"
|
||||
postcss "^5.0.14"
|
||||
|
||||
postcss-modules@^0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-0.5.2.tgz#9d682fed3f282bd64b2aa4feb6f22a2af435ffda"
|
||||
dependencies:
|
||||
css-modules-loader-core "^1.0.1"
|
||||
generic-names "^1.0.1"
|
||||
postcss "^5.1.2"
|
||||
string-hash "^1.1.0"
|
||||
|
||||
postcss-nested@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-1.0.1.tgz#91f28f4e6e23d567241ac154558a0cfab4cc0d8f"
|
||||
@@ -5990,15 +5962,7 @@ postcss-zindex@^2.0.1:
|
||||
postcss "^5.0.4"
|
||||
uniqs "^2.0.0"
|
||||
|
||||
postcss@5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.1.2.tgz#bd84886a66bcad489afaf7c673eed5ef639551e2"
|
||||
dependencies:
|
||||
js-base64 "^2.1.9"
|
||||
source-map "^0.5.6"
|
||||
supports-color "^3.1.2"
|
||||
|
||||
postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.19, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.1.2, postcss@^5.2.13, postcss@^5.2.15, postcss@^5.2.16, postcss@^5.2.17, postcss@^5.2.4, postcss@^5.2.5:
|
||||
postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.19, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.13, postcss@^5.2.15, postcss@^5.2.16, postcss@^5.2.17, postcss@^5.2.4, postcss@^5.2.5:
|
||||
version "5.2.17"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.17.tgz#cf4f597b864d65c8a492b2eabe9d706c879c388b"
|
||||
dependencies:
|
||||
@@ -7277,13 +7241,6 @@ superagent@^2.0.0:
|
||||
qs "^6.1.0"
|
||||
readable-stream "^2.0.5"
|
||||
|
||||
supertest@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/supertest/-/supertest-2.0.1.tgz#a058081d788f1515d4700d7502881e6b759e44cd"
|
||||
dependencies:
|
||||
methods "1.x"
|
||||
superagent "^2.0.0"
|
||||
|
||||
supports-color@3.1.2, supports-color@^3.1.0:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
|
||||
|
||||
Reference in New Issue
Block a user