Merge branch 'master' into 138617379_best_comments

This commit is contained in:
Wyatt Johnson
2017-03-01 16:47:03 -07:00
committed by GitHub
44 changed files with 529 additions and 387 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
FROM node:7.6
# Install yarn
RUN npm install -g yarn
# Add node-gyp for bcrypt build support
RUN yarn global add node-gyp
# Create app directory
RUN mkdir -p /usr/src/app
+25 -1
View File
@@ -1,5 +1,29 @@
import * as actions from '../constants/auth';
import coralApi from '../../../coral-framework/helpers/response';
import coralApi from 'coral-framework/helpers/response';
// Log In.
export const handleLogin = (email, password) => dispatch => {
dispatch({type: actions.LOGIN_REQUEST});
return coralApi('/auth/local', {method: 'POST', body: {email, password}})
.then(result => {
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
})
.catch(error => {
dispatch({type: actions.LOGIN_FAILURE, message: error.translation_key});
});
};
const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUEST});
const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS});
const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE});
export const requestPasswordReset = email => dispatch => {
dispatch(forgotPassowordRequest(email));
return coralApi('/account/password/reset', {method: 'POST', body: {email}})
.then(() => dispatch(forgotPassowordSuccess()))
.catch(error => dispatch(forgotPassowordFailure(error)));
};
// Check Login
@@ -0,0 +1,92 @@
import React, {PropTypes} from 'react';
import Layout from 'coral-admin/src/components/ui/Layout';
import styles from './NotFound.css';
import {Button, TextField, Alert, Success} from 'coral-ui';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
class AdminLogin extends React.Component {
constructor (props) {
super(props);
this.state = {email: '', password: '', requestPassword: false};
}
handleSignIn = e => {
e.preventDefault();
this.props.handleLogin(this.state.email, this.state.password);
}
handleRequestPassword = e => {
e.preventDefault();
this.props.requestPasswordReset(this.state.email);
}
render () {
const {errorMessage} = this.props;
const signInForm = (
<form onSubmit={this.handleSignIn}>
{errorMessage && <Alert>{lang.t(`errors.${errorMessage}`)}</Alert>}
<TextField
label='Email Address'
value={this.state.email}
onChange={e => this.setState({email: e.target.value})} />
<TextField
label='Password'
value={this.state.password}
onChange={e => this.setState({password: e.target.value})}
type='password' />
<div style={{height: 10}}></div>
<Button
type='submit'
cStyle='black'
full
onClick={this.handleSignIn}>Sign In</Button>
<p className={styles.forgotPasswordCTA}>
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={e => {
e.preventDefault();
this.setState({requestPassword: true});
}}>Request a new one.</a>
</p>
</form>
);
const requestPasswordForm = (
this.props.passwordRequestSuccess
? <p className={styles.passwordRequestSuccess} onClick={() => {
location.href = location.href;
}}>
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
<Success />
</p>
: <form onSubmit={this.handleRequestPassword}>
<TextField
label='Email Address'
value={this.state.email}
onChange={e => this.setState({email: e.target.value})} />
<Button
type='submit'
cStyle='black'
full
onClick={this.handleRequestPassword}>Reset Password</Button>
</form>
);
return (
<Layout fixedDrawer restricted={true}>
<div className={styles.loginLayout}>
<h1 className={styles.loginHeader}>Team sign in</h1>
<p className={styles.loginCTA}>Sign in to interact with your community.</p>
{ this.state.requestPassword ? requestPasswordForm : signInForm }
</div>
</Layout>
);
}
}
AdminLogin.propTypes = {
handleLogin: PropTypes.func.isRequired,
passwordRequestSuccess: PropTypes.string,
loginError: PropTypes.string
};
export default AdminLogin;
+30 -5
View File
@@ -1,12 +1,37 @@
.layout {
max-width: 800px;
margin: 0 auto;
max-width: 800px;
margin: 0 auto;
}
.loginLayout {
max-width: 400px;
margin: 0 auto;
}
.loginHeader, .loginCTA, .forgotPasswordCTA, .passwordRequestSuccess {
text-align: center;
font-size: 16px;
}
.forgotPasswordLink, .signInLink {
color: blue;
font-weight: normal;
text-decoration: none;
}
.forgotPasswordLink:hover, .signInLink:hover {
text-decoration: underline;
}
.layout h1 {
font-size: 40px;
font-size: 40px;
}
.layout img {
width: 100%;
.loginHeader {
font-size: 30px;
}
.passwordRequestSuccess {
cursor: pointer;
padding: 8px 14px;
}
@@ -1,13 +0,0 @@
import React from 'react';
import {Layout} from 'react-mdl';
import styles from './NotFound.css';
export const PermissionRequired = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<h1>Permission Required</h1>
<p>Were sorry, but you dont have access to that page.</p>
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
</div>
</Layout>
);
@@ -1,11 +1,11 @@
import React from 'react';
import React, {PropTypes} from 'react';
import {Navigation, Drawer} from 'react-mdl';
import {IndexLink, Link} from 'react-router';
import styles from './Drawer.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
export default ({handleLogout, restricted = false}) => (
const CoralDrawer = ({handleLogout, restricted = false}) => (
<Drawer className={styles.header}>
{ !restricted ?
<div>
@@ -45,5 +45,11 @@ export default ({handleLogout, restricted = false}) => (
</Drawer>
);
CoralDrawer.propTypes = {
handleLogout: PropTypes.func.isRequired,
restricted: PropTypes.bool // hide app elements from a logged out user
};
const lang = new I18n(translations);
export default CoralDrawer;
@@ -1,4 +1,4 @@
import React from 'react';
import React, {PropTypes} from 'react';
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
import {Link, IndexLink} from 'react-router';
import styles from './Header.css';
@@ -6,7 +6,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import {Logo} from './Logo';
export default ({handleLogout, restricted = false}) => (
const CoralHeader = ({handleLogout, restricted = false}) => (
<Header className={styles.header}>
<Logo className={styles.logo} />
{
@@ -64,4 +64,11 @@ export default ({handleLogout, restricted = false}) => (
</Header>
);
CoralHeader.propTypes = {
handleLogout: PropTypes.func.isRequired,
restricted: PropTypes.bool // hide elemnts from a user that's logged out
};
const lang = new I18n(translations);
export default CoralHeader;
+12 -5
View File
@@ -1,15 +1,22 @@
import React from 'react';
import React, {PropTypes} from 'react';
import {Layout as LayoutMDL} from 'react-mdl';
import Header from './Header';
import Drawer from './Drawer';
import styles from './Layout.css';
export const Layout = ({children, ...props}) => (
const Layout = ({children, handleLogout = () => {}, restricted = false, ...props}) => (
<LayoutMDL fixedDrawer>
<Header {...props} />
<Drawer {...props} />
<div className={styles.layout} >
<Header handleLogout={handleLogout} restricted={restricted} {...props} />
<Drawer handleLogout={handleLogout} restricted={restricted} {...props} />
<div className={styles.layout}>
{children}
</div>
</LayoutMDL>
);
Layout.propTypes = {
handleLogout: PropTypes.func,
restricted: PropTypes.bool // hide elements from a user that's logged out
};
export default Layout;
+8
View File
@@ -7,3 +7,11 @@ export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN';
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE';
@@ -3,7 +3,7 @@
h3 {
color: black;
font-size: 1.76em;
font-size: 1.26em;
font-weight: 500;
}
}
@@ -24,6 +24,10 @@
margin-bottom: 20px;
align-items: flex-start;
min-height: 100px;
h3 {
margin: 0;
}
}
.settingsError {
@@ -118,18 +122,19 @@
}
#bannedWordlist, #suspectWordlist {
width: 100%;
padding: 10px;
display: block;
input {
width: 100%;
}
}
.wordlistHeader {
font-weight: bold;
font-size:18px;
margin-bottom:3px;
.customCSSInput {
width: 100%;
font-size: 14px;
padding: 14px;
letter-spacing: 0.03em;
color: #555;
box-sizing: border-box;
}
.enabledSetting {
@@ -150,7 +155,7 @@
margin-top: 38px;
}
.commentSettingsSection {
.settingsSection {
padding-bottom: 200px;
.action {
display: inline-block;
@@ -8,22 +8,20 @@ import {
updateDomainlist
} from '../../actions/settings';
import {Button, List, Item} from 'coral-ui';
import {Button, List, Item, Card, Spinner} from 'coral-ui';
import styles from './Configure.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import EmbedLink from './EmbedLink';
import CommentSettings from './CommentSettings';
import Wordlist from './Wordlist';
import Domainlist from './Domainlist';
import has from 'lodash/has';
import translations from 'coral-admin/src/translations.json';
import StreamSettings from './StreamSettings';
import ModerationSettings from './ModerationSettings';
import TechSettings from './TechSettings';
class Configure extends Component {
constructor (props) {
super(props);
this.state = {
activeSection: 'comments',
activeSection: 'stream',
changed: false,
errors: {}
};
@@ -70,40 +68,48 @@ class Configure extends Component {
getSection (section) {
const pageTitle = this.getPageTitle(section);
let sectionComponent;
switch(section){
case 'comments':
return <CommentSettings
title={pageTitle}
fetchingSettings={this.props.fetchingSettings}
case 'stream':
sectionComponent = <StreamSettings
settings={this.props.settings}
updateSettings={this.onSettingUpdate}
errors={this.state.errors}
settingsError={this.onSettingError}/>;
case 'embed':
return has(this, 'props.settings.domains.whitelist')
? <div>
<Domainlist
domains={this.props.settings.domains.whitelist}
onChangeDomainlist={this.onChangeDomainlist}/>
<EmbedLink title={pageTitle} />
</div>
: <EmbedLink title={pageTitle} />;
case 'wordlist':
return has(this, 'props.settings.wordlist')
? <Wordlist
bannedWords={this.props.settings.wordlist.banned}
suspectWords={this.props.settings.wordlist.suspect}
onChangeWordlist={this.onChangeWordlist} />
: <p>loading wordlists</p>;
break;
case 'moderation':
sectionComponent = <ModerationSettings
onChangeWordlist={this.onChangeWordlist}
settings={this.props.settings}
updateSettings={this.onSettingUpdate} />;
break;
case 'tech':
sectionComponent = <TechSettings
onChangeDomainlist={this.onChangeDomainlist}
settings={this.props.settings}
updateSettings={this.onSettingUpdate} />;
}
if (this.props.settings.fetchingSettings) {
return <Card shadow="4"><Spinner/>Loading settings...</Card>;
}
return (
<div className={styles.settingsSection}>
<h3>{pageTitle}</h3>
{sectionComponent}
</div>
);
}
getPageTitle (section) {
switch(section) {
case 'comments':
return lang.t('configure.comment-settings');
case 'embed':
return lang.t('configure.embed-comment-stream');
case 'stream':
return lang.t('configure.stream-settings');
case 'moderation':
return lang.t('configure.moderation-settings');
case 'tech':
return lang.t('configure.tech-settings');
default:
return '';
}
@@ -120,14 +126,14 @@ class Configure extends Component {
<div className={styles.container}>
<div className={styles.leftColumn}>
<List onChange={this.changeSection} activeItem={activeSection}>
<Item itemId='comments' icon="settings">
{lang.t('configure.comment-settings')}
<Item itemId='stream' icon='speaker_notes'>
{lang.t('configure.stream-settings')}
</Item>
<Item itemId='embed' icon='code'>
{lang.t('configure.embed-comment-stream')}
<Item itemId='moderation' icon='thumbs_up_down'>
{lang.t('configure.moderation-settings')}
</Item>
<Item itemId='wordlist' icon='settings'>
{lang.t('configure.wordlist')}
<Item itemId='tech' icon='code'>
{lang.t('configure.tech-settings')}
</Item>
</List>
<div className={styles.saveBox}>
@@ -9,19 +9,17 @@ const lang = new I18n(translations);
const Domainlist = ({domains, onChangeDomainlist}) => {
return (
<div>
<Card id={styles.domainlist} className={styles.configSetting}>
<h3>{lang.t('configure.domain-list-title')}</h3>
<Card id={styles.domainlist}>
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeDomainlist('whitelist', tags)}
/>
</Card>
</div>
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeDomainlist('whitelist', tags)}
/>
</Card>
);
};
@@ -44,19 +44,15 @@ class EmbedLink extends Component {
"></script>
`.trim();
return (
<div>
<h3>{this.props.title}</h3>
<div>
<Card shadow="2">
<p>{lang.t('configure.copy-and-paste')}</p>
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
{lang.t('embedlink.copy')}
</Button>
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
</Card>
</div>
</div>
<Card shadow="2" className={styles.configSetting}>
<h3>Embed Comment Stream</h3>
<p>{lang.t('configure.copy-and-paste')}</p>
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
{lang.t('embedlink.copy')}
</Button>
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
</Card>
);
}
}
@@ -0,0 +1,73 @@
import React, {PropTypes} from 'react';
import styles from './Configure.css';
import {Card} from 'coral-ui';
import {Checkbox} from 'react-mdl';
import Wordlist from './Wordlist';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
const lang = new I18n(translations);
const updateModeration = (updateSettings, mod) => () => {
const moderation = mod === 'PRE' ? 'POST' : 'PRE';
updateSettings({moderation});
};
const updateEmailConfirmation = (updateSettings, verify) => () => {
updateSettings({requireEmailConfirmation: !verify});
};
const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => {
// just putting this here for shorthand below
const on = styles.enabledSetting;
const off = styles.disabledSetting;
return (
<div>
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
<div className={styles.action}>
<Checkbox
onChange={updateEmailConfirmation(updateSettings, settings.requireEmailConfirmation)}
checked={settings.requireEmailConfirmation} />
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.require-email-verification')}</div>
<p className={settings.requireEmailConfirmation ? '' : styles.disabledSettingText}>
{lang.t('configure.require-email-verification-text')}
</p>
</div>
</Card>
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? on : off}`}>
<div className={styles.action}>
<Checkbox
onChange={updateModeration(updateSettings, settings.moderation)}
checked={settings.moderation === 'PRE'} />
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
<p className={settings.moderation === 'PRE' ? '' : styles.disabledSettingText}>
{lang.t('configure.enable-pre-moderation-text')}
</p>
</div>
</Card>
<Wordlist
bannedWords={settings.wordlist.banned}
suspectWords={settings.wordlist.suspect}
onChangeWordlist={onChangeWordlist} />
</div>
);
};
ModerationSettings.propTypes = {
onChangeWordlist: PropTypes.func.isRequired,
settings: PropTypes.shape({
moderation: PropTypes.string.isRequired,
wordlist: PropTypes.shape({
banned: PropTypes.array.isRequired,
suspect: PropTypes.array.isRequired
})
}).isRequired,
updateSettings: PropTypes.func.isRequired
};
export default ModerationSettings;
@@ -4,7 +4,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import styles from './Configure.css';
import {Textfield, Checkbox} from 'react-mdl';
import {Card, Icon, Spinner} from 'coral-ui';
import {Card, Icon} from 'coral-ui';
const TIMESTAMPS = {
weeks: 60 * 60 * 24 * 7,
@@ -27,15 +27,6 @@ const updateCharCount = (updateSettings, settingsError) => (event) => {
updateSettings({charCount: charCount});
};
const updateModeration = (updateSettings, mod) => () => {
const moderation = mod === 'PRE' ? 'POST' : 'PRE';
updateSettings({moderation});
};
const updateEmailConfirmation = (updateSettings, verify) => () => {
updateSettings({requireEmailConfirmation: !verify});
};
const updateInfoBoxEnable = (updateSettings, infoBox) => () => {
const infoBoxEnable = !infoBox;
updateSettings({infoBoxEnable});
@@ -51,11 +42,6 @@ const updateClosedMessage = (updateSettings) => (event) => {
updateSettings({closedMessage});
};
const updateCustomCssUrl = (updateSettings) => (event) => {
const customCssUrl = event.target.value;
updateSettings({customCssUrl});
};
// If we are changing the measure we need to recalculate using the old amount
// Same thing if we are just changing the amount
const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
@@ -71,44 +57,14 @@ const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
}
};
const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError, settings, errors}) => {
if (fetchingSettings) {
return <Card shadow="4"><Spinner/>Loading settings...</Card>;
}
const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
// just putting this here for shorthand below
const on = styles.enabledSetting;
const off = styles.disabledSetting;
return (
<div className={styles.commentSettingsSection}>
<h3>{title}</h3>
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? on : off}`}>
<div className={styles.action}>
<Checkbox
onChange={updateModeration(updateSettings, settings.moderation)}
checked={settings.moderation === 'PRE'} />
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
<p className={settings.moderation === 'PRE' ? '' : styles.disabledSettingText}>
{lang.t('configure.enable-pre-moderation-text')}
</p>
</div>
</Card>
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
<div className={styles.action}>
<Checkbox
onChange={updateEmailConfirmation(updateSettings, settings.requireEmailConfirmation)}
checked={settings.requireEmailConfirmation} />
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.require-email-verification')}</div>
<p className={settings.requireEmailConfirmation ? '' : styles.disabledSettingText}>
{lang.t('configure.require-email-verification-text')}
</p>
</div>
</Card>
<div>
<Card className={`${styles.configSetting} ${settings.charCountEnable ? on : off}`}>
<div className={styles.action}>
<Checkbox
@@ -193,23 +149,11 @@ const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError
</div>
</div>
</Card>
<Card className={styles.configSettingInfoBox}>
<div className={styles.content}>
{lang.t('configure.custom-css-url')}
<p>{lang.t('configure.custom-css-url-desc')}</p>
<br />
<Textfield
style={{width: '100%'}}
label={lang.t('configure.custom-css-url')}
value={settings.customCssUrl}
onChange={updateCustomCssUrl(updateSettings)} />
</div>
</Card>
</div>
);
};
export default CommentSettings;
export default StreamSettings;
// To see if we are talking about weeks, days or hours
// We talk the remainder of the division and see if it's 0
@@ -0,0 +1,44 @@
import React, {PropTypes} from 'react';
import {Card} from 'coral-ui';
import Domainlist from './Domainlist';
import EmbedLink from './EmbedLink';
import styles from './Configure.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
const lang = new I18n(translations);
const updateCustomCssUrl = (updateSettings) => (event) => {
const customCssUrl = event.target.value;
updateSettings({customCssUrl});
};
const TechSettings = ({settings, onChangeDomainlist, updateSettings}) => {
return (
<div>
<Domainlist
domains={settings.domains.whitelist}
onChangeDomainlist={onChangeDomainlist} />
<EmbedLink />
<Card className={styles.configSetting}>
<h3>{lang.t('configure.custom-css-url')}</h3>
<p>{lang.t('configure.custom-css-url-desc')}</p>
<br />
<input
className={styles.customCSSInput}
value={settings.customCssUrl}
onChange={updateCustomCssUrl(updateSettings)} />
</Card>
</div>
);
};
TechSettings.propTypes = {
settings: PropTypes.shape({
domains: PropTypes.shape({
whitelist: PropTypes.array.isRequired
})
}).isRequired,
updateSettings: PropTypes.func.isRequired
};
export default TechSettings;
@@ -7,9 +7,8 @@ import {Card} from 'coral-ui';
const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
<div>
<h3>{lang.t('configure.banned-words-title')}</h3>
<Card id={styles.bannedWordlist}>
<p className={styles.wordlistHeader}>{lang.t('configure.banned-word-header')}</p>
<Card id={styles.bannedWordlist} className={styles.configSetting}>
<h3>{lang.t('configure.banned-words-title')}</h3>
<p className={styles.wordlistDesc}>{lang.t('configure.banned-word-text')}</p>
<TagsInput
value={bannedWords}
@@ -19,9 +18,8 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
onChange={tags => onChangeWordlist('banned', tags)}
/>
</Card>
<h3>{lang.t('configure.suspect-words-title')}</h3>
<Card id={styles.suspectWordlist}>
<p className={styles.wordlistHeader}>{lang.t('configure.suspect-word-header')}</p>
<Card id={styles.suspectWordlist} className={styles.configSetting}>
<h3>{lang.t('configure.suspect-words-title')}</h3>
<p className={styles.wordlistDesc}>{lang.t('configure.suspect-word-text')}</p>
<TagsInput
value={suspectWords}
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
import {connect} from 'react-redux';
import styles from './style.css';
import {Wizard, WizardNav} from 'coral-ui';
import {Layout} from '../../components/ui/Layout';
import Layout from 'coral-admin/src/components/ui/Layout';
import {
goToStep,
@@ -1,24 +1,33 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {Layout} from '../components/ui/Layout';
import {checkLogin, logout} from '../actions/auth';
import Layout from '../components/ui/Layout';
import {checkLogin, handleLogin, logout, requestPasswordReset} from '../actions/auth';
import {FullLoading} from '../components/FullLoading';
import {PermissionRequired} from '../components/PermissionRequired';
import AdminLogin from '../components/AdminLogin';
class LayoutContainer extends Component {
componentWillMount () {
const {checkLogin} = this.props;
checkLogin().then(() => {
if (!this.props.auth.isAdmin) {
location.href = '/admin/login';
}
});
checkLogin();
}
render () {
const {isAdmin, loggedIn, loadingUser} = this.props.auth;
const {
isAdmin,
loggedIn,
loadingUser,
loginError,
passwordRequestSuccess
} = this.props.auth;
const {handleLogout} = this.props;
if (loadingUser) { return <FullLoading />; }
if (!isAdmin) { return <PermissionRequired />; }
if (isAdmin && loggedIn) { return <Layout {...this.props} />; }
if (!isAdmin) {
return <AdminLogin
handleLogin={this.props.handleLogin}
requestPasswordReset={this.props.requestPasswordReset}
passwordRequestSuccess={passwordRequestSuccess}
errorMessage={loginError} />;
}
if (isAdmin && loggedIn) { return <Layout handleLogout={handleLogout} {...this.props} />; }
return <FullLoading />;
}
}
@@ -29,6 +38,8 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
checkLogin: () => dispatch(checkLogin()),
handleLogin: (username, password) => dispatch(handleLogin(username, password)),
requestPasswordReset: email => dispatch(requestPasswordReset(email)),
handleLogout: () => dispatch(logout())
});
+11 -1
View File
@@ -4,7 +4,9 @@ import * as actions from '../constants/auth';
const initialState = Map({
loggedIn: false,
user: null,
isAdmin: false
isAdmin: false,
loginError: null,
passwordRequestSuccess: null
});
export default function auth (state = initialState, action) {
@@ -25,6 +27,14 @@ export default function auth (state = initialState, action) {
.set('user', action.user);
case actions.LOGOUT_SUCCESS:
return initialState;
case actions.LOGIN_REQUEST:
return state.set('loginError', null);
case actions.LOGIN_FAILURE:
return state.set('loginError', action.message);
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
return state.set('passwordRequestSuccess', null);
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
return state.set('passwordRequestSuccess', 'If you have a registered account, a password reset link was sent to that email.');
default :
return state;
}
+2 -2
View File
@@ -33,7 +33,7 @@ export default function settings (state = initialState, action) {
.set('fetchSettingsError', null);
case actions.SETTINGS_RECEIVED:
return state.merge({
fetchingSettings: null,
fetchingSettings: false,
fetchSettingsError: null,
...action.settings
});
@@ -43,7 +43,7 @@ export default function settings (state = initialState, action) {
.set('fetchSettingsError', action.error);
case actions.SETTINGS_UPDATED:
return state.merge({
fetchingSettings: null,
fetchingSettings: false,
fetchSettingsError: null,
...action.settings
});
+14 -6
View File
@@ -1,5 +1,8 @@
{
"en": {
"errors": {
"NOT_AUTHORIZED": "Your username or password is not recognized by our system."
},
"community": {
"username_and_email": "Username and Email",
"account_creation_date": "Account Creation Date",
@@ -50,6 +53,9 @@
"copy": "Copy to Clipboard"
},
"configure": {
"stream-settings": "Stream Settings",
"moderation-settings": "Moderation Settings",
"tech-settings": "Tech Settings",
"custom-css-url": "Custom CSS URL",
"custom-css-url-desc": "URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external.",
"dashboard": "Dashboard",
@@ -62,8 +68,6 @@
"include-text": "Include your text here.",
"comment-settings": "Settings",
"embed-comment-stream": "Embed Stream",
"banned-word-header": "Write the banned words list",
"suspect-word-header": "Write the suspect words list",
"banned-word-text": "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
"suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
"wordlist": "Banned Words",
@@ -85,7 +89,7 @@
"comment-count-text-pre": "Comments will be limited to ",
"comment-count-text-post": " characters.",
"comment-count-error": "Please enter a valid number.",
"domain-list-title": "Domain Whitelist",
"domain-list-title": "Permitted Domains",
"domain-list-text": "Enter the domains you would like to permit for Talk, e.g. your local, staging and production environments (ex. localhost:3000, staging.domain.com, domain.com)."
},
"bandialog": {
@@ -134,6 +138,9 @@
}
},
"es": {
"errors": {
"NOT_AUTHORIZED": "Acción no autorizada."
},
"community": {
"username_and_email": "Usuario y E-mail",
"account_creation_date": "Fecha de creación de la cuenta",
@@ -171,6 +178,9 @@
"username_flags": ""
},
"configure": {
"stream-settings": "Configuración de Comentarios",
"moderation-settings": "Configuración de Moderación",
"tech-settings": "Configuración Technical",
"custom-css-url": "URL CSS a medida",
"custom-css-url-desc": "URL de una hoja de estilo que va a sobrescribir los estilos por defecto de Embed Stream. Puede ser interna o externa.",
"dashboard": "Panel",
@@ -184,8 +194,6 @@
"comment-settings": "Configuración de Comentarios",
"embed-comment-stream": "Colocar Hilo de Comentarios",
"wordlist": "Palabras Suspendidas y Suspechosas",
"banned-word-header": "Escribir las palabras no permitidas",
"suspect-word-header": "Write the suspect words list",
"banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente separadas de los comentarios publicados.",
"suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
"banned-words-title": "Banned words list",
@@ -215,7 +223,7 @@
},
"bandialog": {
"ban_user": "Quieres suspender el Usuario?",
"are_you_sure": "Estas segura que quieres suspender a {props.author.username}?",
"are_you_sure": "Estas segura que quieres suspender a {0}?",
"note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.",
"cancel": "Cancelar",
"yes_ban_user": "Si, Suspendan el usuario"
+2 -2
View File
@@ -83,8 +83,8 @@ class Embed extends Component {
}
}
setActiveReplyBox (reactKey) {
if (!this.props.currentUser) {
setActiveReplyBox = (reactKey) => {
if (!this.props.auth.user) {
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
this.props.showSignInDialog(offset);
} else {
+26 -13
View File
@@ -5,24 +5,30 @@ const Coral = {};
const Talk = Coral.Talk = {};
// build the URL to load in the pym iframe
function buildStreamIframeUrl(talkBaseUrl, asset, comment) {
function buildStreamIframeUrl(talkBaseUrl, asset_url, comment, asset_id) {
let iframeArray = [
talkBaseUrl,
(talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/'
'embed/stream?asset_url=',
encodeURIComponent(asset)
encodeURIComponent(asset_url)
];
if (comment) {
iframeArray.push('&comment_id=');
iframeArray.push(encodeURIComponent(comment));
}
if (asset_id) {
iframeArray.push('&asset_id=');
iframeArray.push(encodeURIComponent(asset_id));
}
return iframeArray.join('');
}
// Set up postMessage listeners/handlers on the pymParent
// e.g. to resize the iframe, and navigate the host page
function configurePymParent(pymParent, assetUrl) {
function configurePymParent(pymParent, asset_url) {
let notificationOffset = 200;
let ready = false;
@@ -52,7 +58,7 @@ function configurePymParent(pymParent, assetUrl) {
window.clearInterval(interval);
// @todo - It's weird to me that this is sent here in addition to the iframe URL. Could it just be in one place?
pymParent.sendMessage('DOMContentLoaded', assetUrl);
pymParent.sendMessage('DOMContentLoaded', asset_url);
}
}, 100);
});
@@ -88,11 +94,11 @@ function configurePymParent(pymParent, assetUrl) {
* @param {Object} opts - Configuration options for talk
* @param {String} opts.talk - Talk base URL
* @param {String} [opts.title] - Title of Stream (rendered in iframe)
* @param {String} [opts.asset] - parent Asset ID or URL. Comments in the
* stream will replies to this asset
* @param {String} [opts.asset_url] - Asset URL
* @param {String} [opts.asset_id] - Asset ID
*/
Talk.render = function (el, opts) {
if ( ! el) {
if (!el) {
throw new Error('Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.');
}
if (typeof el !== 'object') {
@@ -101,7 +107,7 @@ Talk.render = function (el, opts) {
opts = opts || {};
// @todo infer this URL without explicit user input (if possible, may have to be added at build/render time of this script)
if (! opts.talk) {
if (!opts.talk) {
throw new Error('Coral.Talk.render() expects opts.talk as the Talk Base URL');
}
@@ -110,16 +116,23 @@ Talk.render = function (el, opts) {
el.id = `_${Math.random()}`;
}
let asset = opts.asset || window.location.href.split('#')[0];
let asset_url = opts.asset_url || window.location.href.split('#')[0];
let comment = window.location.hash.slice(1);
let pymParent = new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, asset, comment), {
let query = {
title: opts.title,
asset_url: asset,
asset_url: asset_url,
id: `${el.id}_iframe`,
name: `${el.id}_iframe`
});
};
configurePymParent(pymParent, asset);
if (opts.asset_id && opts.asset_id.length > 0) {
query.asset_id = opts.asset_id;
}
let pymParent = new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, asset_url, comment), query);
configurePymParent(pymParent, asset_url);
};
export default Coral;
@@ -42,7 +42,7 @@ export const postComment = graphql(POST_COMMENT, {
updateQueries: {
AssetQuery: (oldData, {mutationResult:{data:{createComment:{comment}}}}) => {
if (oldData.asset.moderation === 'PRE') {
if (oldData.asset.settings.moderation === 'PRE') {
return oldData;
}
@@ -14,8 +14,8 @@ function getQueryVariable(variable) {
}
}
// If no query is included, return a default string for development
return 'http://localhost/default/stream';
// If not found, return null.
return null;
}
export const getCounts = (data) => ({asset_id, limit, sort}) => {
@@ -85,12 +85,19 @@ export const loadMore = (data) => ({limit, cursor, parent_id, asset_id, sort}, n
};
export const queryStream = graphql(STREAM_QUERY, {
options: () => ({
variables: {
asset_url: getQueryVariable('asset_url'),
comment_id: getQueryVariable('comment_id')
}
}),
options: () => {
let comment_id = getQueryVariable('comment_id');
let has_comment = comment_id != null;
return {
variables: {
asset_id: getQueryVariable('asset_id'),
asset_url: getQueryVariable('asset_url'),
comment_id: has_comment ? comment_id : 'no-comment',
has_comment
}
};
},
props: ({data}) => ({
data,
loadMore: loadMore(data),
@@ -3,6 +3,11 @@ query myCommentHistory {
comments {
id
body
asset {
id
title
url
}
created_at
}
}
@@ -1,7 +1,7 @@
#import "../fragments/commentView.graphql"
query AssetQuery($asset_url: String!, $comment_id: ID!) {
comment(id: $comment_id) {
query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comment: Boolean!) {
comment(id: $comment_id) @include(if: $has_comment) {
...commentView
parent {
...commentView
@@ -10,7 +10,7 @@ query AssetQuery($asset_url: String!, $comment_id: ID!) {
}
}
}
asset(url: $asset_url) {
asset(id: $asset_id, url: $asset_url) {
id
title
url
+3 -2
View File
@@ -5,7 +5,7 @@ const Comment = props => {
return (
<div className={styles.myComment}>
<p className="myCommentAsset">
<a className={`${styles.assetURL} myCommentAnchor`} href='#' onClick={props.link(`${props.asset.url}#${props.comment.id}`)}>{props.asset.url}</a>
<a className={`${styles.assetURL} myCommentAnchor`} href='#' onClick={props.link(`${props.asset.url}#${props.comment.id}`)}>{props.asset.title ? props.asset.title : props.asset.url}</a>
</p>
<p className={`${styles.commentBody} myCommentBody`}>{props.comment.body}</p>
</div>
@@ -18,7 +18,8 @@ Comment.propTypes = {
body: PropTypes.string
}).isRequired,
asset: PropTypes.shape({
url: PropTypes.string
url: PropTypes.string,
title: PropTypes.string
}).isRequired
};
@@ -11,7 +11,7 @@ const CommentHistory = props => {
key={i}
comment={comment}
link={props.link}
asset={props.asset} />;
asset={comment.asset} />;
})}
</div>
</div>
@@ -19,8 +19,7 @@ const CommentHistory = props => {
};
CommentHistory.propTypes = {
comments: PropTypes.array.isRequired,
asset: PropTypes.object.isRequired
comments: PropTypes.array.isRequired
};
export default CommentHistory;
@@ -5,9 +5,9 @@ import translations from '../translations';
import I18n from 'coral-framework/modules/i18n/i18n';
const lang = new I18n(translations);
export default ({showSignInDialog}) => (
export default ({showSignInDialog, requireEmailConfirmation}) => (
<div className={styles.message}>
<SignInContainer noButton={true}/>
<SignInContainer noButton={true} requireEmailConfirmation={requireEmailConfirmation}/>
<div>
<a onClick={() => {
showSignInDialog();
@@ -35,7 +35,7 @@ class ProfileContainer extends Component {
const {me} = this.props.data;
if (!loggedIn || !me) {
return <NotLoggedIn showSignInDialog={showSignInDialog}/>;
return <NotLoggedIn showSignInDialog={showSignInDialog} requireEmailConfirmation={asset.settings.requireEmailConfirmation}/>;
}
if (data.loading) {
@@ -1,8 +1,7 @@
import React from 'react';
import TextField from 'coral-ui/components/TextField';
import Alert from './Alert';
import Button from 'coral-ui/components/Button';
import {Dialog} from 'coral-ui';
import {Dialog, Alert} from 'coral-ui';
import FakeComment from './FakeComment';
import styles from './styles.css';
@@ -1,6 +1,5 @@
import React, {PropTypes} from 'react';
import Alert from './Alert';
import {Button, TextField, Spinner, Success} from 'coral-ui';
import {Button, TextField, Spinner, Success, Alert} from 'coral-ui';
import styles from './styles.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
@@ -1,6 +1,5 @@
import React, {PropTypes} from 'react';
import Alert from './Alert';
import {Button, TextField, Spinner, Success} from 'coral-ui';
import {Button, TextField, Spinner, Success, Alert} from 'coral-ui';
import styles from './styles.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
@@ -65,23 +65,6 @@ input.error{
padding: 3px 0 16px;
}
.alert {
padding: 10px;
margin-bottom: 20px;
border-radius: 2px;
}
.alert--success {
border: solid 1px #1ec00e;
background: #cbf1b8;
color: #006900;
}
.alert--error {
background: #FFEBEE;
color: #B71C1C;
}
.userBox {
padding: 10px 0 20px;
letter-spacing: 0.1px;
+16
View File
@@ -0,0 +1,16 @@
.alert {
padding: 10px;
margin-bottom: 20px;
border-radius: 2px;
}
.alert--success {
border: solid 1px #1ec00e;
background: #cbf1b8;
color: #006900;
}
.alert--error {
background: #FFEBEE;
color: #B71C1C;
}
@@ -1,5 +1,5 @@
import React from 'react';
import styles from './styles.css';
import styles from './Alert.css';
const Alert = ({cStyle = 'error', children, className, ...props}) => (
<div
+1
View File
@@ -1,3 +1,4 @@
export {default as Alert} from './components/Alert';
export {default as Dialog} from './components/Dialog';
export {default as CoralLogo} from './components/CoralLogo';
export {default as FabButton} from './components/FabButton';
+3 -1
View File
@@ -1,6 +1,7 @@
const errors = require('../../errors');
const AssetsService = require('../../services/assets');
const ActionsService = require('../../services/actions');
const CommentsService = require('../../services/comments');
const Wordlist = require('../../services/wordlist');
@@ -146,10 +147,11 @@ const createPublicComment = (context, commentInput) => {
// TODO: this is kind of fragile, we should refactor this to resolve
// all these const's that we're using like 'COMMENTS', 'FLAG' to be
// defined in a checkable schema.
return context.mutators.Action.create({
return ActionsService.insertUserAction({
item_id: comment.id,
item_type: 'COMMENTS',
action_type: 'FLAG',
user_id: null,
group_id: 'Matched suspect word filter',
metadata: {}
})
-4
View File
@@ -15,10 +15,6 @@ router.get('/password-reset', (req, res) => {
res.render('password-reset', {redirectUri: process.env.TALK_ROOT_URL});
});
router.get('/login', (req, res, next) => {
res.render('admin/login');
});
router.get('*', (req, res) => {
res.render('admin', {basePath: '/client/coral-admin'});
});
@@ -20,6 +20,10 @@ describe('coral-plugin-history/CommentHistory', () => {
'closedAt':null
};
comments.forEach((comment) => {
comment.asset = asset;
});
beforeEach(() => {
render = shallow(<CommentHistory comments={comments} asset={asset} link={()=>{}}/>);
});
-132
View File
@@ -1,132 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<title>Admin Login</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
<style media="screen">
body, #root {
width: 100%;
height: 100%;
margin: 0;
background: #fff;
}
#root form {
max-width: 300px;
border: 1px solid lightgrey;
box-shadow: 0px 10px 24px 2px rgba(0,0,0,0.2);
margin: 50px auto;
padding: 15px;
}
.legend {
text-align: center;
width: 100%;
font-weight: bold;
}
label {
display: block;
margin-top: 10px;
margin-bottom: 3px;
padding-right: 30px;
}
small {
color: #888;
}
input {
border-radius: 4px;
margin-top: 3px;
border: 1px solid lightgrey;
font-size: 16px;
width: 100%;
padding: 14px;
height: 100%;
display: inline-block;
}
.submit-password-reset {
border-radius: 4px;
border: none;
display: block;
background-color: #333;
color: white;
text-align: center;
width: 100%;
padding: 10px;
margin-top: 10px;
cursor: pointer;
}
.error-console {
display: none;
margin-top: 10px;
border-radius: 4px;
background-color: pink;
color: red;
border: 1px solid red;
padding: 10px;
}
.error-console.active {
display: block;
}
</style>
</head>
<body>
<div id="root">
<form id="login-form">
<legend class="legend">Admin Login</legend>
<label for="email">
Email
<input type="email" name="email" placeholder=""/>
</label>
<label for="password">
Password
<input type="password" name="password" placeholder="" />
</label>
<button class="submit-password-reset" type="submit">Login</button>
<div class="error-console"></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 handleSubmit (e) {
e.preventDefault();
$('.error-console').removeClass('active');
var password = $('[name="password"]').val();
var email = $('[name="email"]').val();
$.ajax({
url: '/api/v1/auth/local',
contentType: 'application/json',
method: 'POST',
headers: {
'X-CSRF-Token': '<%= csrfToken %>'
},
data: JSON.stringify({password: password, email: email})
}).then(function (success) {
location.href = '/admin';
}).catch(function (error) {
showError(error.responseText);
});
}
$('#login-form').on('submit', handleSubmit);
});
</script>
</body>
</html>
+3 -2
View File
@@ -17,7 +17,7 @@ const buildEmbeds = [
];
module.exports = {
devtool: 'cheap-source-map',
devtool: '#cheap-module-source-map',
entry: Object.assign({}, {
'embed': [
'babel-polyfill',
@@ -58,7 +58,8 @@ module.exports = {
exclude: /node_modules/,
test: /\.js$/,
query: {
cacheDirectory: true
cacheDirectory: true,
sourceMap: true
}
},
{