Merge branch 'master' into guides

This commit is contained in:
David Erwin
2016-11-28 12:45:25 -05:00
committed by GitHub
68 changed files with 735 additions and 331 deletions
-2
View File
@@ -5,5 +5,3 @@
- click a button
- view the cat
- see the cat meow
@coralproject/tech
+1
View File
@@ -7,3 +7,4 @@ dist/coral-admin/bundle.js
*.iml
.env
gaba.cfg
.idea/
+3 -3
View File
@@ -21,7 +21,7 @@ if (app.get('env') !== 'test') {
// APP MIDDLEWARE
//==============================================================================
app.set('trust proxy', 'loopback');
app.set('trust proxy', 1);
app.use(helmet());
app.use(bodyParser.json());
app.use('/client', express.static(path.join(__dirname, 'dist')));
@@ -97,7 +97,7 @@ app.use('/api', (err, req, res, next) => {
res.status(err.status || 500);
res.json({
message: err.message,
error: app.get('env') === 'development' ? err : null
error: app.get('env') === 'development' ? err : {}
});
});
@@ -109,7 +109,7 @@ app.use('/', (err, req, res, next) => {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: app.get('env') === 'development' ? err : null
error: app.get('env') === 'development' ? err : {}
});
});
+4 -4
View File
@@ -1,14 +1,14 @@
import React from 'react';
import {Router, Route, IndexRoute, browserHistory} from 'react-router';
import ModerationQueue from 'containers/ModerationQueue';
import CommentStream from 'containers/CommentStream';
import Configure from 'containers/Configure';
import ModerationQueue from 'containers/ModerationQueue/ModerationQueue';
import CommentStream from 'containers/CommentStream/CommentStream';
import Configure from 'containers/Configure/Configure';
import CommunityContainer from 'containers/Community/CommunityContainer';
import LayoutContainer from 'containers/LayoutContainer';
const routes = (
<Route path='admin' component={LayoutContainer}>
<Route path='/admin' component={LayoutContainer}>
<IndexRoute component={ModerationQueue} />
<Route path='embed' component={CommentStream} />
<Route path='community' component={CommunityContainer} />
+19
View File
@@ -0,0 +1,19 @@
import * as actions from '../constants/auth';
import {base, handleResp, getInit} from '../helpers/response';
// Check Login
const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST});
const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin});
const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
fetch(`${base}/auth`, getInit('GET'))
.then(handleResp)
.then(user => {
const isAdmin = !!user.roles.filter(i => i === 'admin').length;
dispatch(checkLoginSuccess(user, isAdmin));
})
.catch(error => dispatch(checkLoginFailure(error)));
};
+8 -7
View File
@@ -1,11 +1,12 @@
import React from 'react';
import {Button, Icon} from 'react-mdl';
import timeago from 'timeago.js';
import styles from './CommentList.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import Linkify from 'react-linkify';
import {FabButton} from 'coral-ui';
import {Icon} from 'react-mdl';
const linkify = new Linkify();
@@ -14,7 +15,7 @@ export default props => {
const links = linkify.getMatches(props.comment.get('body'));
return (
<li tabindex={props.index} className={`${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<li tabIndex={props.index} className={`${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<i className={`material-icons ${styles.avatar}`}>person</i>
@@ -26,12 +27,12 @@ export default props => {
{links ?
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={styles.actions}>
{props.actions.map(action => canShowAction(action, props.comment) ? (
<Button className={styles.actionButton}
{props.actions.map((action, i) => canShowAction(action, props.comment) ? (
<FabButton icon={props.actionsMap[action].icon} className={styles.actionButton}
cStyle={action}
key={i}
onClick={() => props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))}
fab colored>
<Icon name={props.actionsMap[action].icon} />
</Button>
/>
) : null)}
</div>
</div>
@@ -112,15 +112,15 @@ export default class CommentList extends React.Component {
}
render () {
const {singleView, commentIds, comments, hideActive} = this.props;
const {singleView, commentIds, comments, hideActive, key} = this.props;
const {active} = this.state;
return (
<ul className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
<ul className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}>
{commentIds.map((commentId, index) => (
<Comment comment={comments.get(commentId)}
ref={el => { if (el && commentId === active) { this._active = el; } }}
key={`${index }comment`}
key={index}
index={index}
onClickAction={this.onClickAction}
actions={this.props.actions}
@@ -1,27 +0,0 @@
import React from 'react';
import {Layout, Navigation, Drawer, Header} from 'react-mdl';
import {Link} from 'react-router';
import styles from './Header.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
// App header. If we add a navbar it should be here
export default (props) => (
<Layout fixedDrawer>
<Header title='Talk'>
<Navigation>
<Link className={styles.navLink} to={'/admin/'}>{lang.t('configure.moderate')}</Link>
<Link className={styles.navLink} to={'/admin/configure'}>{lang.t('Configure')}</Link>
</Navigation>
</Header>
<Drawer>
<Navigation>
<Link className={styles.navLink} to={'/admin/'}>{lang.t('configure.moderate')}</Link>
<Link className={styles.navLink} to={'/admin/configure'}>{lang.t('configure.Configure')}</Link>
</Navigation>
</Drawer>
{props.children}
</Layout>
);
const lang = new I18n(translations);
@@ -0,0 +1,12 @@
.layout {
max-width: 800px;
margin: 0 auto;
}
.layout h1 {
font-size: 40px;
}
.layout img {
width: 100%;
}
@@ -0,0 +1,13 @@
import React from 'react';
import {Layout} from 'react-mdl';
import styles from './NotFound.css';
export const NotFound = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<h1>Page Not Found</h1>
<p>The communicorn feels your pain.</p>
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
</div>
</Layout>
);
@@ -0,0 +1,13 @@
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>
);
@@ -0,0 +1,21 @@
.header {
background: #505050;
overflow: hidden;
}
.header > div {
position: relative;
padding: 0;
width: 1170px;
margin: 0 auto;
}
.active {
background: #232323;
}
.version {
position: absolute;
right: 0;
width: 50px;
}
+10 -8
View File
@@ -1,20 +1,22 @@
import React from 'react';
import {Navigation, Header} from 'react-mdl';
import {Link} from 'react-router';
import {Link, IndexLink} from 'react-router';
import styles from './Header.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import {Logo} from './Logo';
export default () => (
<Header title='Talk'>
<Header className={styles.header}>
<Logo />
<Navigation>
<Link className={styles.navLink} to="/admin">{lang.t('configure.moderate')}</Link>
<Link className={styles.navLink} to="/admin/community">{lang.t('configure.community')}</Link>
<Link className={styles.navLink} to="/admin/configure">{lang.t('configure.configure')}</Link>
<span>
{`v${process.env.VERSION}`}
</span>
<IndexLink className={styles.navLink} to="/admin" activeClassName={styles.active}>{lang.t('configure.moderate')}</IndexLink>
<Link className={styles.navLink} to="/admin/community" activeClassName={styles.active}>{lang.t('configure.community')}</Link>
<Link className={styles.navLink} to="/admin/configure" activeClassName={styles.active}>{lang.t('configure.configure')}</Link>
</Navigation>
<div className={styles.version}>
{`v${process.env.VERSION}`}
</div>
</Header>
);
@@ -0,0 +1,18 @@
.logo h1 {
color: #272727;
font-size: 20px;
padding: 0 30px;
}
.logo span {
display: inline-block;
margin-left: 10px;
font-size: 18px;
vertical-align: middle;
}
.logo {
background: #E5E5E5;
}
@@ -0,0 +1,12 @@
import React from 'react';
import styles from './Logo.css';
import {CoralLogo} from 'coral-ui';
export const Logo = () => (
<div className={styles.logo}>
<h1>
<CoralLogo stroke="#E5E5E5" />
<span>Talk</span>
</h1>
</div>
);
+7
View File
@@ -0,0 +1,7 @@
export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
@@ -31,7 +31,7 @@ class CommentStream extends React.Component {
// The only action for now is flagging
onClickAction (action, id) {
if (action === 'flagged') {
if (action === 'flag') {
this.props.dispatch(flagComment(id));
clearTimeout(this._snackTimeout);
this.setState({snackbar: true, snackbarMsg: 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.'});
@@ -1,7 +1,6 @@
import React from 'react';
import {connect} from 'react-redux';
import {fetchSettings, updateSettings, saveSettingsToServer} from '../actions/settings';
import {fetchSettings, updateSettings, saveSettingsToServer} from '../../actions/settings';
import {
List,
ListItem,
@@ -14,7 +13,7 @@ import {
} from 'react-mdl';
import styles from './Configure.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import translations from '../../translations.json';
class Configure extends React.Component {
constructor (props) {
@@ -23,9 +22,13 @@ class Configure extends React.Component {
this.state = {activeSection: 'comments', copied: false};
this.copyToClipBoard = this.copyToClipBoard.bind(this);
// Update settings
this.updateModeration = this.updateModeration.bind(this);
// InfoBox has two settings. Enable or not and the content of it if it is enable.
this.updateInfoBoxEnable = this.updateInfoBoxEnable.bind(this);
this.updateInfoBoxContent = this.updateInfoBoxContent.bind(this);
this.saveSettings = this.saveSettings.bind(this);
}
@@ -100,7 +103,7 @@ class Configure extends React.Component {
}
getEmbed () {
const embedText = `<div id='coralStreamEmbed'></div><script type='text/javascript' src='http://pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embed/stream', {});</script>`;
const embedText = `<div id='coralStreamEmbed'></div><script type='text/javascript' src='${window.location.protocol}//pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embed/stream', {title: 'Comments'});</script>`;
return <List>
<ListItem className={styles.configSettingEmbed}>
@@ -1,19 +1,41 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {Layout} from '../components/ui/Layout';
import {checkLogin} from '../actions/auth';
import {NotFound} from '../components/NotFound';
import {PermissionRequired} from '../components/PermissionRequired';
class LayoutContainer extends Component {
componentWillMount () {
this.props.checkLogin();
}
render () {
const {isAdmin, loggedIn} = this.props.auth;
if (!loggedIn) {
return <NotFound />;
}
if (!isAdmin && loggedIn) {
return <PermissionRequired />;
}
return <Layout {...this.props} />;
}
}
LayoutContainer.propTypes = {};
const mapStateToProps = () => ({});
const mapStateToProps = state => ({
auth: state.auth.toJS()
});
const mapDispatchToProps = (dispatch) => ({dispatch});
const mapDispatchToProps = dispatch => ({
checkLogin: () => dispatch(checkLogin()),
});
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
export default connect(
mapStateToProps,
mapDispatchToProps
)(LayoutContainer);
@@ -1,4 +1,3 @@
@custom-media --big-viewport (min-width: 780px);
.listContainer {
@@ -6,8 +5,13 @@
margin: 0 auto;
}
.tabBar {
background: #9E9E9E;
}
.tab {
flex: 1;
color: white;
}
@media (--big-viewport) {
@@ -1,16 +1,22 @@
import React from 'react';
import {connect} from 'react-redux';
import key from 'keymaster';
import ModerationKeysModal from 'components/ModerationKeysModal';
import CommentList from 'components/CommentList';
import {updateStatus} from 'actions/comments';
import styles from './ModerationQueue.css';
import key from 'keymaster';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import translations from '../../translations.json';
/*
* Renders the moderation queue as a tabbed layout with 3 moderation
* queues filtered by status (Untouched, Rejected and Approved)
* queues :
* * pending: filtered by status Untouched
* * rejected: filtered by status Rejected
* * flagged: with a flagged action on them
*/
class ModerationQueue extends React.Component {
@@ -61,9 +67,9 @@ class ModerationQueue extends React.Component {
return (
<div>
<div className='mdl-tabs mdl-js-tabs mdl-js-ripple-effect'>
<div className='mdl-tabs__tab-bar'>
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<a href='#pending' onClick={() => this.onTabClick('pending')}
className={`mdl-tabs__tab is-active ${styles.tab}`}>{lang.t('modqueue.pending')}</a>
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.pending')}</a>
<a href='#rejected' onClick={() => this.onTabClick('rejected')}
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.rejected')}</a>
<a href='#flagged' onClick={() => this.onTabClick('flagged')}
+8 -5
View File
@@ -1,12 +1,15 @@
export const base = '/api/v1';
export const getInit = (method, body) => {
const headers = new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json'
});
let init = {
method,
headers: new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json'
}),
credentials: 'same-origin'
};
const init = {method, headers};
if (method.toLowerCase() !== 'get') {
init.body = JSON.stringify(body);
}
+28
View File
@@ -0,0 +1,28 @@
import {Map} from 'immutable';
import * as actions from '../constants/auth';
const initialState = Map({
loggedIn: false,
user: null,
isAdmin: false
});
export default function auth (state = initialState, action) {
switch (action.type) {
case actions.CHECK_LOGIN_FAILURE:
return state
.set('loggedIn', false)
.set('user', null);
case actions.CHECK_LOGIN_SUCCESS:
return state
.set('loggedIn', true)
.set('isAdmin', action.isAdmin)
.set('user', action.user);
case actions.LOGOUT_SUCCESS:
return state
.set('loggedIn', false)
.set('user', null);
default :
return state;
}
}
+3 -1
View File
@@ -2,11 +2,13 @@ import {combineReducers} from 'redux';
import comments from 'reducers/comments';
import settings from 'reducers/settings';
import community from 'reducers/community';
import auth from 'reducers/auth';
// Combine all reducers into a main one
export default combineReducers({
settings,
comments,
community
community,
auth
});
@@ -37,7 +37,7 @@ const fetchModerationQueueComments = store =>
Promise.all([
fetch('/api/v1/queue/comments/pending'),
fetch('/api/v1/comments?status=rejected'),
fetch('/api/v1/comments?action=flag')
fetch('/api/v1/comments?action_type=flag')
])
.then(res => Promise.all(res.map(r => r.json())))
.then(res => {
@@ -52,7 +52,7 @@ Promise.all([
const updateComment = (store, comment) => {
fetch(`/api/v1/comments/${comment.get('id')}/status`, {
method: 'POST',
method: 'PUT',
headers: jsonHeader,
body: JSON.stringify({status: comment.get('status')})
})
+29 -14
View File
@@ -61,8 +61,8 @@ class CommentStream extends Component {
// Set up messaging between embedded Iframe an parent component
// Using recommended Pym init code which violates .eslint standards
const pym = new Pym.Child({polling: 100});
const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl)[1];
this.props.getStream(path);
const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl);
this.props.getStream(path && path[1] || window.location);
}
render () {
@@ -84,8 +84,9 @@ class CommentStream extends Component {
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
const {loggedIn, user} = this.props.auth;
return <div>
const {actions, users, comments} = this.props.items;
const {loggedIn, user, showSignInDialog} = this.props.auth;
return <div className={showSignInDialog ? 'expandForSignin' : ''}>
{
rootItem
? <div>
@@ -105,26 +106,27 @@ class CommentStream extends Component {
id={rootItemId}
premod={this.props.config.moderation}
reply={false}
canPost={loggedIn}
author={user}
/>
{!loggedIn && <SignInContainer />}
</div>
{
rootItem.comments && rootItem.comments.map((commentId) => {
const comment = this.props.items.comments[commentId];
const comment = comments[commentId];
return <div className="comment" key={commentId}>
<hr aria-hidden={true}/>
<AuthorName name={comment.username}/>
<AuthorName author={users[comment.author_id]}/>
<PubDate created_at={comment.created_at}/>
<Content body={comment.body}/>
<div className="commentActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}/>
id={commentId}
showReply={comment.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={commentId}
like={this.props.items.actions[comment.like]}
like={actions[comment.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
@@ -135,7 +137,7 @@ class CommentStream extends Component {
<FlagButton
addNotification={this.props.addNotification}
id={commentId}
flag={this.props.items.actions[comment.flag]}
flag={actions[comment.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
@@ -151,6 +153,7 @@ class CommentStream extends Component {
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
premod={this.props.config.moderation}
showReply={comment.showReply}/>
@@ -160,13 +163,14 @@ class CommentStream extends Component {
let reply = this.props.items.comments[replyId];
return <div className="reply" key={replyId}>
<hr aria-hidden={true}/>
<AuthorName name={reply.username}/>
<AuthorName author={users[reply.author_id]}/>
<PubDate created_at={reply.created_at}/>
<Content body={reply.body}/>
<div className="replyActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={replyId}/>
id={replyId}
showReply={reply.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={replyId}
@@ -188,10 +192,21 @@ class CommentStream extends Component {
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
comment_id={reply.comment_id}
asset_id={reply.comment_id}
comment_id={reply.parent_id}
asset_id={rootItemId}
/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
child_id={replyId}
premod={this.props.config.moderation}
showReply={reply.showReply}/>
</div>;
})
}
+6 -2
View File
@@ -3,8 +3,12 @@ body {
font-family: 'Open Sans', sans-serif;
width: 100%;
font-size: 12px;
margin: 0;
min-height: 700px;
margin: 0px;
padding: 0px 0px 50px 0px;
}
.expandForSignin {
min-height: 550px;
}
button {
+20 -3
View File
@@ -3,6 +3,7 @@ import translations from './../translations';
const lang = new I18n(translations);
import * as actions from '../constants/auth';
import {base, handleResp, getInit} from '../helpers/response';
import {addItem} from './items';
// Dialog Actions
export const showSignInDialog = () => ({type: actions.SHOW_SIGNIN_DIALOG});
@@ -19,8 +20,8 @@ export const cleanState = () => ({type: actions.CLEAN_STATE});
// Sign In Actions
const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST});
const signInSuccess = (user) => ({type: actions.FETCH_SIGNIN_SUCCESS, user});
const signInFailure = (error) => ({type: actions.FETCH_SIGNIN_FAILURE, error});
const signInSuccess = user => ({type: actions.FETCH_SIGNIN_SUCCESS, user});
const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
export const fetchSignIn = (formData) => dispatch => {
dispatch(signInRequest());
@@ -29,6 +30,7 @@ export const fetchSignIn = (formData) => dispatch => {
.then(({user}) => {
dispatch(hideSignInDialog());
dispatch(signInSuccess(user));
dispatch(addItem(user, 'users'));
})
.catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError'))));
};
@@ -54,8 +56,10 @@ export const facebookCallback = (err, data) => dispatch => {
return;
}
try {
dispatch(signInFacebookSuccess(JSON.parse(data)));
const user = JSON.parse(data);
dispatch(signInFacebookSuccess(user));
dispatch(hideSignInDialog());
dispatch(addItem(user, 'users'));
} catch (err) {
dispatch(signInFacebookFailure(err));
return;
@@ -114,3 +118,16 @@ export const logout = () => dispatch => {
export const validForm = () => ({type: actions.VALID_FORM});
export const invalidForm = error => ({type: actions.INVALID_FORM, error});
// Check Login
const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST});
const checkLoginSuccess = user => ({type: actions.CHECK_LOGIN_SUCCESS, user});
const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
fetch(`${base}/auth`, getInit('GET'))
.then(handleResp)
.then(user => dispatch(checkLoginSuccess(user)))
.catch(error => dispatch(checkLoginFailure(error)));
};
+10 -2
View File
@@ -106,8 +106,16 @@ export function getStream (assetUrl) {
/* Add items to the store */
const itemTypes = Object.keys(json);
for (let i = 0; i < itemTypes.length; i++ ) {
for (let j = 0; j < json[itemTypes[i]].length; j++ ) {
dispatch(addItem(json[itemTypes[i]][j], itemTypes[i]));
if (itemTypes[i] === 'actions') {
for (let j = 0; j < json[itemTypes[i]].length; j++ ) {
let action = json[itemTypes[i]][j];
action.id = `${action.action_type}_${action.item_id}`;
dispatch(addItem(action, 'actions'));
}
} else {
for (let j = 0; j < json[itemTypes[i]].length; j++ ) {
dispatch(addItem(json[itemTypes[i]][j], itemTypes[i]));
}
}
}
+4
View File
@@ -27,3 +27,7 @@ export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
export const INVALID_FORM = 'INVALID_FORM';
export const VALID_FORM = 'VALID_FORM';
export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
+10 -1
View File
@@ -37,6 +37,14 @@ export default function auth (state = initialState, action) {
case actions.FETCH_SIGNIN_REQUEST:
return state
.set('isLoading', true);
case actions.CHECK_LOGIN_FAILURE:
return state
.set('loggedIn', false)
.set('user', null);
case actions.CHECK_LOGIN_SUCCESS:
return state
.set('loggedIn', true)
.set('user', action.user);
case actions.FETCH_SIGNIN_SUCCESS:
return state
.set('loggedIn', true)
@@ -44,7 +52,8 @@ export default function auth (state = initialState, action) {
case actions.FETCH_SIGNIN_FAILURE:
return state
.set('isLoading', false)
.set('error', action.error);
.set('error', action.error)
.set('user', null);
case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
return state
.set('user', action.user)
@@ -1,9 +1,9 @@
import React from 'react';
const packagename = 'coral-plugin-author-name';
const AuthorName = ({name}) =>
const AuthorName = ({author}) =>
<div className={`${packagename}-text`}>
{name}
{author && author.displayName}
</div>;
export default AuthorName;
+7 -6
View File
@@ -12,7 +12,8 @@ class CommentBox extends Component {
id: PropTypes.string,
comments: PropTypes.array,
reply: PropTypes.bool,
canPost: PropTypes.bool
canPost: PropTypes.bool,
currentUser: PropTypes.object
}
state = {
@@ -21,11 +22,11 @@ class CommentBox extends Component {
}
postComment = () => {
const {postItem, updateItem, id, parent_id, addNotification, appendItemArray, premod} = this.props;
const {postItem, updateItem, id, parent_id, child_id, addNotification, appendItemArray, premod, author} = this.props;
let comment = {
body: this.state.body,
asset_id: id,
username: this.state.username
author_id: author.id
};
let related;
let parent_type;
@@ -37,7 +38,7 @@ class CommentBox extends Component {
related = 'comments';
parent_type = 'assets';
}
updateItem(parent_id, 'showReply', false, 'comments');
updateItem(child_id || parent_id, 'showReply', false, 'comments');
postItem(comment, 'comments')
.then((comment_id) => {
if (premod === 'pre') {
@@ -52,7 +53,7 @@ class CommentBox extends Component {
}
render () {
const {styles, reply, canPost} = this.props;
const {styles, reply, author} = this.props;
// How to handle language in plugins? Should we have a dependency on our central translation file?
return <div>
<div
@@ -73,7 +74,7 @@ class CommentBox extends Component {
rows={3}/>
</div>
<div className={`${name}-button-container`}>
{ canPost && (
{ author && (
<button
className={`${name}-button`}
style={styles && styles.button}
+6 -3
View File
@@ -4,11 +4,14 @@ import translations from './translations.json';
const name = 'coral-plugin-flags';
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification}) => {
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification, currentUser}) => {
const flagged = flag && flag.current_user;
const onFlagClick = () => {
if (!currentUser) {
return;
}
if (!flagged) {
postAction(id, 'flag', '123', 'comments')
postAction(id, 'flag', currentUser.id, 'comments')
.then((action) => {
let id = `${action.action_type}_${action.item_id}`;
addItem({id, current_user: action, count: flag ? flag.count + 1 : 1}, 'actions');
@@ -32,7 +35,7 @@ const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, ad
: <span className={`${name}-button-text`}>{lang.t('flag')}</span>
}
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
style={flagged && styles.flaggedIcon}
style={flagged ? styles.flaggedIcon : {}}
aria-hidden={true}>flag</i>
</button>
</div>;
+5 -2
View File
@@ -4,11 +4,14 @@ import translations from './translations.json';
const name = 'coral-plugin-flags';
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem}) => {
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem, currentUser}) => {
const liked = like && like.current_user;
const onLikeClick = () => {
if (!currentUser) {
return;
}
if (!liked) {
postAction(id, 'like', '123', 'comments')
postAction(id, 'like', currentUser.id, 'comments')
.then((action) => {
let id = `${action.action_type}_${action.item_id}`;
addItem({id, current_user: action, count: like ? like.count + 1 : 1}, 'actions');
+1 -1
View File
@@ -6,7 +6,7 @@ const name = 'coral-plugin-replies';
const ReplyButton = (props) => <button
className={`${name}-reply-button`}
onClick={() => props.updateItem(props.id || props.parent_id, 'showReply', true, 'comments')}>
onClick={() => props.updateItem(props.id, 'showReply', !props.showReply, 'comments')}>
{lang.t('reply')}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>reply</i>
+1 -2
View File
@@ -106,7 +106,7 @@ input.error{
.userBox a {
color: #2c69b6;
cursor: pointer;
margin: 0 5px;
margin: 0px;
}
.attention {
@@ -140,4 +140,3 @@ input.error{
border: 1px solid orange;
background-color: 1px solid coral
}
@@ -18,7 +18,8 @@ import {
fetchForgotPassword,
facebookCallback,
invalidForm,
validForm
validForm,
checkLogin
} from '../../coral-framework/actions/auth';
class SignInContainer extends Component {
@@ -43,6 +44,10 @@ class SignInContainer extends Component {
this.addError = this.addError.bind(this);
}
componentWillMount () {
this.props.checkLogin();
}
componentDidMount() {
window.authCallback = this.props.facebookCallback;
const {formData} = this.state;
@@ -147,6 +152,7 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
checkLogin: () => dispatch(checkLogin()),
facebookCallback: (err, data) => dispatch(facebookCallback(err, data)),
fetchSignUp: formData => dispatch(fetchSignUp(formData)),
fetchSignIn: formData => dispatch(fetchSignIn(formData)),
+42
View File
@@ -0,0 +1,42 @@
import React, {PropTypes} from 'react';
const CoralLogo = ({height = '30px', width = '30px', stroke = '#FFFFFF'}) => (
<svg width={width} height={height} viewBox='0 0 381 391' version='1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink'>
<g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
<g id='Wordmark-Round' transform='translate(-1833.000000, -411.000000)' stroke={stroke} strokeWidth='22' strokeLinecap='round' strokeLinejoin='round'>
<g id='coralProjectLogo-2-Copy-2' transform='translate(1842.000000, 421.000000)'>
<g id='Layer_2' transform='translate(2.268750, 1.133903)'>
<rect id='Rectangle-1' fill='#F47E6B' x='0' y='0' width='358.4625' height='368.518519' rx='40'>
</rect>
<path d='M0.226875,105.679772 C13.7259375,122.688319 41.29125,131.986325 56.71875,116.792023 C67.0415625,106.586895 62.5040625,93.0934473 76.910625,63.3851852 C83.94375,48.7578348 90.523125,38.3259259 101.980313,37.5321937 C115.139063,36.6250712 131.814375,50.3452991 134.31,69.3948718 C137.826563,95.1344729 115.479375,105.339601 121.15125,123.822222 C127.390313,144.119088 157.110938,140.377208 166.52625,165.096296 C170.042813,174.394302 171.630938,190.382336 163.463438,198.319658 C149.170313,212.266667 120.584063,186.073504 80.7675,198.319658 C73.280625,200.587464 56.3784375,205.803419 51.500625,219.523647 C46.73625,233.130484 55.584375,251.046154 67.60875,260.797721 C93.245625,281.661538 119.79,254.674644 159.379688,271.909972 C181.840313,281.661538 203.053125,303.319088 208.725,330.305983 C211.674375,344.593162 209.6325,356.952707 207.704063,364.549858' id='Shape'>
</path>
</g>
<g id='Layer_3' transform='translate(43.106250, 289.145299)'>
<path d='M90.4096875,72.5698006 C78.8390625,41.3874644 64.9996875,31.4091168 54.1096875,28.234188 C41.8584375,24.7190883 30.7415625,29.0279202 17.8096875,21.2039886 C8.394375,15.4210826 3.403125,6.57663818 0.680625,0'id='Shape'></path>
</g><g id='Layer_4' transform='translate(220.068750, 209.772080)'>
<path d='M81.7884375,152.963533 C74.9821875,122.007977 61.8234375,104.77265 50.593125,94.5675214 C31.8759375,77.6723647 14.746875,77.1054131 5.218125,57.2621083 C4.4240625,55.5612536 -5.7853125,33.4501425 5.218125,17.008547 C14.52,3.06153846 33.350625,1.47407407 41.518125,0.907122507 C64.3190625,-0.907122507 73.280625,9.52478632 100.959375,13.039886 C117.180938,15.0809117 130.793438,13.4934473 139.30125,12.0193732'id='Shape'></path>
</g>
<g id='Layer_5' transform='translate(285.862500, 289.145299)'>
<path d='M74.415,2.04102564 C66.3609375,0.793732194 51.046875,-0.566951567 33.12375,5.1025641 C17.5828125,9.97834758 6.80625,18.0290598 0.9075,23.2450142'id='Shape'></path>
</g>
<g id='Layer_6' transform='translate(174.693750, 1.133903)'>
<path d='M184.109063,151.035897 C170.950313,174.507692 158.699063,180.290598 149.850938,181.311111 C133.85625,183.011966 129.091875,168.724786 104.475938,163.168661 C79.2928125,157.499145 72.2596875,169.745299 52.0678125,163.168661 C30.0609375,155.911681 18.7171875,134.821083 12.705,123.822222 C-1.588125,97.4022792 -0.3403125,71.6626781 0.5671875,51.2524217 C1.588125,29.4814815 6.125625,12.0193732 9.6421875,0.907122507'id='Shape'></path>
<path d='M183.541875,69.3948718 C170.496563,56.6951567 157.564688,45.8096866 149.28375,39.0062678 C143.385,34.1304843 134.990625,33.2233618 128.297813,36.8518519 C128.070938,36.9652422 127.844063,37.0786325 127.730625,37.1920228 C122.739375,40.1401709 119.449688,45.3561254 118.8825,51.1390313 C118.201875,59.0763533 117.0675,71.4358974 114.912188,82.8883191 C114.118125,87.0837607 107.085,103.865527 89.7290625,110.668946 C77.2509375,115.544729 62.0503125,110.668946 53.4290625,101.597721 C40.38375,87.7641026 44.8078125,66.9002849 47.416875,55.2210826 C53.5425,26.760114 73.3940625,9.07122507 82.6959375,1.81424501'id='Shape'></path>
</g>
<g id='Layer_7' transform='translate(3.403125, 179.156695)'>
<path d='M0.1134375,0.226780627 C2.949375,6.34985755 8.394375,16.1014245 18.2634375,25.3994302 C27.67875,34.2438746 37.3209375,39.0062678 43.4465625,41.5008547'id='Shape'></path>
</g>
</g>
</g>
</g>
</svg>
);
CoralLogo.propTypes = {
height: PropTypes.string,
width: PropTypes.string,
stroke: PropTypes.string
};
export default CoralLogo;
+9
View File
@@ -0,0 +1,9 @@
.type--approve {
background: #00796b;
color: rgba(255, 255, 255, 0.901961);
}
.type--reject {
background: #d32f2f ;
color: rgba(255, 255, 255, 0.901961);
}
+11
View File
@@ -0,0 +1,11 @@
import React from 'react';
import styles from './FabButton.css';
import {FABButton, Icon} from 'react-mdl';
const FabButton = ({cStyle = 'local', icon, className, ...props}) => (
<FABButton className={`${styles[`type--${cStyle}`]} ${className ? className : ''}`} {...props}>
<Icon name={icon} />
</FABButton>
);
export default FabButton;
+2
View File
@@ -1 +1,3 @@
export {default as Dialog} from './components/Dialog';
export {default as CoralLogo} from './components/CoralLogo';
export {default as FabButton} from './components/FabButton';
+27 -17
View File
@@ -2,7 +2,9 @@
* authorization contains the references to the authorization middleware.
* @type {Object}
*/
const authorization = module.exports = {};
const authorization = module.exports = {
middleware: []
};
const debug = require('debug')('talk:middleware:authorization');
@@ -33,21 +35,29 @@ authorization.has = (user, ...roles) => roles.every((role) => user.roles.indexOf
* @param {Array} roles all the roles that a user must have
* @return {Callback} connect middleware
*/
authorization.needed = (...roles) => (req, res, next) => {
// All routes that are wrapepd with this middleware actually require a role.
if (!req.user) {
debug(`No user on request, returning with ${ErrNotAuthorized}`);
return next(ErrNotAuthorized);
}
authorization.needed = (...roles) => [
// Check to see if the current user has all the roles requested for the given
// array of roles requested, if one is not on the user, then this will
// evaluate to true.
if (!authorization.has(req.user, ...roles)) {
debug('User does not have all the required roles to access this page');
return next(ErrNotAuthorized);
}
// Insert the pre-needed middlware.
...authorization.middleware,
// Looks like they're allowed!
return next();
};
// Insert the actual middleware to check for the required role.
(req, res, next) => {
// All routes that are wrapepd with this middleware actually require a role.
if (!req.user) {
debug(`No user on request, returning with ${ErrNotAuthorized}`);
return next(ErrNotAuthorized);
}
// Check to see if the current user has all the roles requested for the given
// array of roles requested, if one is not on the user, then this will
// evaluate to true.
if (!authorization.has(req.user, ...roles)) {
debug('User does not have all the required roles to access this page');
return next(ErrNotAuthorized);
}
// Looks like they're allowed!
return next();
}
];
+93 -25
View File
@@ -27,6 +27,35 @@ ActionSchema.statics.findById = function(id) {
return Action.findOne({id});
};
/**
* Add an action.
* @param {String} item_id identifier of the comment (uuid)
* @param {String} user_id user id of the action (uuid)
* @param {String} action the new action to the comment
* @return {Promise}
*/
ActionSchema.statics.insertUserAction = ({item_id, item_type, user_id, action_type}) => {
const action = {
item_id,
item_type,
user_id,
action_type
};
// Create/Update the action.
return Action.findOneAndUpdate(action, action, {
// Ensure that if it's new, we return the new object created.
new: true,
// Perform an upsert in the event that this doesn't exist.
upsert: true,
// Set the default values if not provided based on the mongoose models.
setDefaultsOnInsert: true
});
};
/**
* Finds actions in an array of ids.
* @param {String} ids array of user identifiers (uuid)
@@ -41,32 +70,71 @@ ActionSchema.statics.findByItemIdArray = function(item_ids) {
* Returns summaries of actions for an array of ids
* @param {String} ids array of user identifiers (uuid)
*/
ActionSchema.statics.getActionSummaries = function(item_ids) {
return ActionSchema.statics.findByItemIdArray(item_ids).then((rawActions) => {
// Create an object with a count of each action type for each item
const actionSummaries = rawActions.reduce((actionObj, action) => {
if (!actionObj[action.item_id]) {
actionObj[action.item_id] = {
id: action.id,
item_type: action.item_type,
action_type: action.action_type,
count: 1,
current_user: false //Update this later when we have authentication
};
} else {
actionObj[action.item_id].count ++;
}
return actionObj;
}, {});
ActionSchema.statics.getActionSummaries = function(item_ids, current_user_id = '') {
return Action.aggregate([
{
// Return an array extracted from the actionSummaries object
return Object.keys(actionSummaries).reduce((actions, key) => {
let actionSummary = actionSummaries[key];
actionSummary.item_id = key;
actions.push(actionSummary);
return actions;
}, []);
});
// only grab items that match the specified item id's
$match: {
item_id: {
$in: item_ids
}
}
},
{
$group: {
// group unique documents by these properties, we are leveraging the
// fact that each uuid is completly unique.
_id: {
item_id: '$item_id',
action_type: '$action_type'
},
// and sum up all actions matching the above grouping criteria
count: {
$sum: 1
},
// we are leveraging the fact that each uuid is completly unique and
// just grabbing the last instance of the item type here.
item_type: {
$last: '$item_type'
},
current_user: {
$max: {
$cond: {
if: {
$eq: ['$user_id', current_user_id],
},
then: '$$CURRENT',
else: null
}
}
}
}
},
{
$project: {
// suppress the _id field
_id: false,
// map the fields from the _id grouping down a level
item_id: '$_id.item_id',
action_type: '$_id.action_type',
// map the field directly
count: '$count',
item_type: '$item_type',
// set the current user to false here
current_user: '$current_user'
}
}
])
.exec();
};
/*
+8 -11
View File
@@ -157,20 +157,17 @@ CommentSchema.statics.changeStatus = function(id, status) {
/**
* Add an action to the comment.
* @param {String} id identifier of the comment (uuid)
* @param {String} item_id identifier of the comment (uuid)
* @param {String} user_id user id of the action (uuid)
* @param {String} action the new action to the comment
* @return {Promise}
*/
CommentSchema.statics.addAction = function(id, user_id, action_type) {
// check that the comment exist
let action = new Action({
action_type: action_type,
item_type: 'comment',
item_id: id,
user_id: user_id
});
return action.save();
};
CommentSchema.statics.addAction = (item_id, user_id, action_type) => Action.insertUserAction({
item_id,
item_type: 'comment',
user_id,
action_type
});
//==============================================================================
// Remove Statics
+11 -1
View File
@@ -402,7 +402,7 @@ UserService.findById = (id) => {
};
/**
* Finds users in an array of idd.
* Finds users in an array of ids.
* @param {Array} ids array of user identifiers (uuid)
*/
UserService.findByIdArray = (ids) => {
@@ -411,6 +411,16 @@ UserService.findByIdArray = (ids) => {
});
};
/**
* Finds public user information by an array of ids.
* @param {Array} ids array of user identifiers (uuid)
*/
UserService.findPublicByIdArray = (ids) => {
return UserModel.find({
'id': {$in: ids}
}, 'id displayName');
};
/**
* Creates a JWT from a user email. Only works for local accounts.
* @param {String} email of the local user
+3 -7
View File
@@ -1,14 +1,10 @@
const express = require('express');
const router = express.Router();
router.get('/embed/stream/preview', (req, res) => {
res.render('embed-stream', {basePath: '/client/embed/stream'});
});
// this route is expecting there to be a token in the hash
// see /views/password-reset-email.ejs
// Get /password-reset expects a signed token (JWT) in the hash.
// Links to this endpoint are generated by /views/password-reset-email.ejs.
router.get('/password-reset', (req, res, next) => {
// TODO: store the redirect uri in the token or something fancy
// TODO: store the redirect uri in the token or something fancy.
// admins and regular users should probably be redirected to different places.
res.render('password-reset', {redirectUri: process.env.TALK_ROOT_URL});
});
+2 -1
View File
@@ -6,7 +6,8 @@ const router = express.Router();
router.delete('/:action_id', (req, res, next) => {
Action
.findOneAndRemove({
id: req.params.action_id
id: req.params.action_id,
user_id: req.user.id
})
.then(() => {
res.status(204).end();
-11
View File
@@ -30,15 +30,4 @@ router.get('/:id', (req, res, next) => {
});
// Upsert an asset and return the affected document.
router.put('/', (req, res, next) => {
Asset.upsert(req.body)
.then((asset) => {
res.json(asset);
})
.catch(next);
});
module.exports = router;
+2 -2
View File
@@ -8,13 +8,13 @@ const router = express.Router();
* This returns the user if they are logged in.
*/
router.get('/', authorization.needed(), (req, res) => {
res.json(req.user);
res.json(req.user.toObject());
});
/**
* This destroys the session of a user, if they have one.
*/
router.delete('/', (req, res) => {
router.delete('/', authorization.needed(), (req, res) => {
req.session.destroy(() => {
res.status(204).end();
});
+8 -9
View File
@@ -1,10 +1,11 @@
const express = require('express');
const Comment = require('../../../models/comment');
const wordlist = require('../../../services/wordlist');
const authorization = require('../../../middleware/authorization');
const router = express.Router();
router.get('/', (req, res, next) => {
router.get('/', authorization.needed('admin'), (req, res, next) => {
let query;
if (req.query.status) {
@@ -28,8 +29,7 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
const {
body,
asset_id,
parent_id,
author_id
parent_id
} = req.body;
Comment
@@ -38,7 +38,7 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
asset_id,
parent_id,
status: req.wordlist.matched ? 'rejected' : '',
author_id
author_id: req.user.id
})
.then((comment) => {
@@ -49,7 +49,7 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
});
});
router.get('/:comment_id', (req, res, next) => {
router.get('/:comment_id', authorization.needed('admin'), (req, res, next) => {
Comment
.findById(req.params.comment_id)
.then(comment => {
@@ -65,7 +65,7 @@ router.get('/:comment_id', (req, res, next) => {
});
});
router.delete('/:comment_id', (req, res, next) => {
router.delete('/:comment_id', authorization.needed('admin'), (req, res, next) => {
Comment
.removeById(req.params.comment_id)
.then(() => {
@@ -76,7 +76,7 @@ router.delete('/:comment_id', (req, res, next) => {
});
});
router.put('/:comment_id/status', (req, res, next) => {
router.put('/:comment_id/status', authorization.needed('admin'), (req, res, next) => {
const {
status
@@ -95,12 +95,11 @@ router.put('/:comment_id/status', (req, res, next) => {
router.post('/:comment_id/actions', (req, res, next) => {
const {
user_id,
action_type
} = req.body;
Comment
.addAction(req.params.comment_id, user_id, action_type)
.addAction(req.params.comment_id, req.user.id, action_type)
.then((action) => {
res.status(201).json(action);
})
+4 -3
View File
@@ -1,14 +1,15 @@
const express = require('express');
const authorization = require('../../middleware/authorization');
const router = express.Router();
router.use('/asset', require('./asset'));
router.use('/auth', require('./auth'));
router.use('/comments', require('./comments'));
router.use('/comments', authorization.needed(), require('./comments'));
router.use('/queue', require('./queue'));
router.use('/settings', require('./settings'));
router.use('/settings', authorization.needed('admin'), require('./settings'));
router.use('/stream', require('./stream'));
router.use('/user', require('./user'));
router.use('/actions', require('./actions'));
router.use('/actions', authorization.needed(), require('./actions'));
module.exports = router;
+2 -2
View File
@@ -1,7 +1,7 @@
const express = require('express');
const Comment = require('../../../models/comment');
const Setting = require('../../../models/setting');
const authorization = require('../../../middleware/authorization');
const router = express.Router();
@@ -13,7 +13,7 @@ const router = express.Router();
// depending on the settings. The :moderation overwrites this settings.
// Pre-moderation: New comments are shown in the moderator queues immediately.
// Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users.
router.get('/comments/pending', (req, res, next) => {
router.get('/comments/pending', authorization.needed('admin'), (req, res, next) => {
Setting.getModerationSetting().then(function({moderation}){
Comment.moderationQueue(moderation).then((comments) => {
res.status(200).json(comments);
+3 -2
View File
@@ -1,7 +1,8 @@
const _ = require('lodash');
const express = require('express');
const router = express.Router();
const Setting = require('../../../models/setting');
const _ = require('lodash');
const router = express.Router();
router.get('/', (req, res, next) => {
Setting
+1 -1
View File
@@ -41,7 +41,7 @@ router.get('/', (req, res, next) => {
asset.id,
...comments.map((comment) => comment.id),
...comments.map((comment) => comment.author_id)
]))
]), req.user ? req.user.id : '')
]);
})
.then(([assets, comments, users, actions]) => {
+3 -2
View File
@@ -7,8 +7,9 @@ const fs = require('fs');
const path = require('path');
const resetEmailFile = fs.readFileSync(path.resolve(__dirname, '../../../views/password-reset-email.ejs'));
const resetEmailTemplate = ejs.compile(resetEmailFile.toString());
const authorization = require('../../../middleware/authorization');
router.get('/', (req, res, next) => {
router.get('/', authorization.needed('admin'), (req, res, next) => {
const {
value = '',
field = 'created_at',
@@ -49,7 +50,7 @@ router.get('/', (req, res, next) => {
.catch(next);
});
router.post('/:user_id/role', (req, res, next) => {
router.post('/:user_id/role', authorization.needed('admin'), (req, res, next) => {
User
.addRoleToUser(req.params.user_id, req.body.role)
.then(role => {
+2 -2
View File
@@ -342,9 +342,9 @@ definitions:
type: string
format: date-time
description: Display name of comment
user_id:
author_id:
type: string
description: Display name of comment
description: User who posted the comment
parent_id:
type: string
description: Display name of comment
@@ -29,21 +29,23 @@ describe('itemActions', () => {
],
actions: [
{
type: 'like',
id: '123',
action_type: 'like',
item_id: '123',
count: 1,
id: 'like_123',
current_user: false
},
{
type: 'flag',
id: '456',
action_type: 'flag',
item_id: '456',
count: 5,
id: 'flag_456',
current_user: true
}
]
};
it('should get an stream from an asset_id and send the appropriate dispatches', () => {
it('should get an stream from an asset_url and send the appropriate dispatches', () => {
fetchMock.get('*', JSON.stringify(response));
return actions.getStream(assetUrl)(store.dispatch)
.then((res) => {
+52 -22
View File
@@ -5,23 +5,25 @@ const expect = require('chai').expect;
describe('Action: models', () => {
let mockActions;
beforeEach(() => {
return Action.create([{
action_type: 'flag',
item_id: '123',
item_type: 'comments'
}, {
action_type: 'like',
item_id: '789',
item_type: 'comments'
item_type: 'comment',
user_id: 'flagginguserid'
}, {
action_type: 'flag',
item_id: '456',
item_type: 'comments'
item_type: 'comment'
}, {
action_type: 'flag',
item_id: '123',
item_type: 'comments'
item_type: 'comment'
}, {
action_type: 'like',
item_id: '123',
item_type: 'comment'
}]).then((actions) => {
mockActions = actions;
});
@@ -30,8 +32,7 @@ describe('Action: models', () => {
describe('#findById()', () => {
it('should find an action by id', () => {
return Action.findById(mockActions[0].id).then((result) => {
expect(result).to.have.property('action_type')
.and.to.equal('flag');
expect(result).to.have.property('action_type', 'flag');
});
});
});
@@ -39,33 +40,62 @@ describe('Action: models', () => {
describe('#findByItemIdArray()', () => {
it('should find an array of actions from an array of item_ids', () => {
return Action.findByItemIdArray(['123', '456']).then((result) => {
expect(result).to.have.length(3);
expect(result).to.have.length(4);
});
});
});
describe('#getActionSummaries()', () => {
it('should return properly formatted summaries from an array of item_ids', () => {
return Action.getActionSummaries(['123', '789']).then((result) => {
expect(result).to.have.length(2);
const sorted = result.sort((a, b) => a.count - b.count);
delete sorted[0].id;
delete sorted[1].id;
expect(sorted[0]).to.deep.equal({
return Action.getActionSummaries(['123', '789']).then((summaries) => {
expect(summaries).to.have.length(2);
expect(summaries).to.deep.include({
action_type: 'like',
count: 1,
item_id: '789',
item_type: 'comments',
current_user: false
item_id: '123',
item_type: 'comment',
current_user: null
});
expect(sorted[1]).to.deep.equal({
expect(summaries).to.deep.include({
action_type: 'flag',
count: 2,
item_id: '123',
item_type: 'comments',
current_user: false
item_type: 'comment',
current_user: null
});
});
});
it('should include a current user when one is passed', () => {
return Action
.getActionSummaries(['123'], 'flagginguserid')
.then((summaries) => {
expect(summaries).to.have.length(2);
let summary = summaries.find((s) => s.item_id === '123' && s.action_type === 'flag');
expect(summary).to.not.be.undefined;
expect(summary.current_user).to.not.be.null;
expect(summary.current_user).to.have.property('item_id', '123');
expect(summary.current_user).to.have.property('item_type', 'comment');
expect(summary.current_user).to.have.property('user_id', 'flagginguserid');
expect(summary.current_user).to.have.property('action_type', 'flag');
});
});
it('should not include a current user when one is passed for a user that doesn\'t have an action', () => {
return Action
.getActionSummaries(['123'], 'flagginguserid2')
.then((summaries) => {
expect(summaries).to.have.length(2);
summaries.forEach((summary) => {
expect(summary).to.not.be.undefined;
expect(summary).to.have.property('current_user', null);
});
});
});
});
});
+16
View File
@@ -43,6 +43,22 @@ describe('User: models', () => {
});
});
describe('#findPublicByIdArray()', () => {
it('should find an array of users from an array of ids', () => {
const ids = mockUsers.map((user) => user.id);
return User.findPublicByIdArray(ids).then((result) => {
expect(result).to.have.length(3);
const sorted = result.sort((a, b) => {
if(a.displayName < b.displayName) {return -1;}
if(a.displayName > b.displayName) {return 1;}
return 0;
});
expect(sorted[0]).to.have.property('displayName')
.and.to.equal('Marvel');
});
});
});
describe('#findLocalUser', () => {
it('should find a user when we give the right credentials', () => {
+2 -61
View File
@@ -1,22 +1,13 @@
require('../../../utils/mongoose');
const passport = require('../../../utils/passport');
const chai = require('chai');
const expect = chai.expect;
const server = require('../../../../app');
// Setup chai.
chai.should();
chai.use(require('chai-http'));
let fixture = {
'url': 'http://hhgg.com/total-perspective-vortex',
'type': 'article',
'headline': 'The Total Perspective Vortex',
'summary': 'You are an insignificant dot on an insignificant dot.',
'section': 'Everything',
'authors': ['Ford Prefect']
};
describe('Asset: routes', () => {
describe('/GET Asset', () => {
@@ -25,6 +16,7 @@ describe('Asset: routes', () => {
chai.request(server)
.get('/api/v1/asset')
.set(passport.inject({roles: ['admin']}))
.end((err, res) => {
if (err) {
@@ -41,55 +33,4 @@ describe('Asset: routes', () => {
});
});
// This test checks PUT and read
describe('/PUT Asset', () => {
describe('#put', () => {
it('It should save an asset and load it again.', (done) => {
chai.request(server)
.put('/api/v1/asset')
.send(fixture)
.end((err, res) => {
if (err) {
throw new Error(err);
}
res.should.have.status(200);
res.body.should.be.a('object');
// Id should be generated by the model if absent.
res.body.should.have.property('id');
// Save the asset id to compare with GET result.
let assetId = res.body.id;
// Load the asset to make sure it's really there.
chai.request(server)
.get(`/api/v1/asset?url=${encodeURIComponent(fixture.url)}`)
.end((err, res) => {
if (err) {
throw new Error(err);
}
res.should.have.status(200);
res.body.should.be.an('array');
let asset = res.body[0];
expect(asset).to.have.property('id');
// Ensure the asset has the same id as above.
// This tests the single url per Id concept.
expect(assetId).to.equal(asset.id);
done();
});
});
});
});
}); // End describe /PUT Asset
});
+25 -5
View File
@@ -1,6 +1,7 @@
process.env.NODE_ENV = 'test';
require('../../../utils/mongoose');
const passport = require('../../../utils/passport');
const app = require('../../../../app');
const chai = require('chai');
@@ -68,6 +69,7 @@ describe('Get /comments', () => {
it('should return all the comments', () => {
return chai.request(app)
.get('/api/v1/comments')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
@@ -126,6 +128,7 @@ describe('Get comments by status and action', () => {
it('should return all the rejected comments', () => {
return chai.request(app)
.get('/api/v1/comments?status=rejected')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
expect(res.body[0]).to.have.property('id', 'abc');
@@ -135,6 +138,7 @@ describe('Get comments by status and action', () => {
it('should return all the approved comments', () => {
return chai.request(app)
.get('/api/v1/comments?status=accepted')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
expect(res.body[0]).to.have.property('id', 'hij');
@@ -144,6 +148,7 @@ describe('Get comments by status and action', () => {
it('should return all the new comments', () => {
return chai.request(app)
.get('/api/v1/comments?status=new')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
expect(res.body[0]).to.have.property('id', 'def');
@@ -153,6 +158,7 @@ describe('Get comments by status and action', () => {
it('should return all the flagged comments', () => {
return chai.request(app)
.get('/api/v1/comments?action_type=flag')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
@@ -195,6 +201,7 @@ describe('Post /comments', () => {
it('should create a comment', () => {
return chai.request(app)
.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': '1', 'parent_id': ''})
.then((res) => {
expect(res).to.have.status(201);
@@ -205,6 +212,7 @@ describe('Post /comments', () => {
it('should create a comment with a rejected status if it contains a bad word', () => {
return chai.request(app)
.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'bad words are the baddest', 'author_id': '123', 'asset_id': '1', 'parent_id': ''})
.then((res) => {
expect(res).to.have.status(201);
@@ -262,6 +270,7 @@ describe('Get /:comment_id', () => {
it('should return the right comment for the comment_id', () => {
return chai.request(app)
.get('/api/v1/comments/abc')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(200);
expect(res).to.have.property('body');
@@ -318,6 +327,7 @@ describe('Remove /:comment_id', () => {
it('it should remove comment', () => {
return chai.request(app)
.delete('/api/v1/comments/abc')
.set(passport.inject({roles: ['admin']}))
.then((res) => {
expect(res).to.have.status(204);
@@ -329,11 +339,6 @@ describe('Remove /:comment_id', () => {
});
});
process.on('unhandledRejection', (reason) => {
console.error('Reason: ');
console.error(reason);
});
describe('Put /:comment_id/status', () => {
const comments = [{
@@ -384,12 +389,26 @@ describe('Put /:comment_id/status', () => {
it('it should update status', function() {
return chai.request(app)
.put('/api/v1/comments/abc/status')
.set(passport.inject({roles: ['admin']}))
.send({status: 'accepted'})
.then((res) => {
expect(res).to.have.status(204);
expect(res.body).to.be.empty;
});
});
it('it should not allow a non-admin to update status', () => {
return chai.request(app)
.put('/api/v1/comments/abc/status')
.set(passport.inject({roles: []}))
.send({status: 'accepted'})
.then((res) => {
expect(res).to.be.empty;
})
.catch((err) => {
expect(err).to.have.property('status', 401);
});
});
});
describe('Post /:comment_id/actions', () => {
@@ -442,6 +461,7 @@ describe('Post /:comment_id/actions', () => {
it('it should update actions', () => {
return chai.request(app)
.post('/api/v1/comments/abc/actions')
.set(passport.inject({id: '456', roles: ['admin']}))
.send({'user_id': '456', 'action_type': 'flag'})
.then((res) => {
expect(res).to.have.status(201);
+2
View File
@@ -1,6 +1,7 @@
process.env.NODE_ENV = 'test';
require('../../../utils/mongoose');
const passport = require('../../../utils/passport');
const app = require('../../../../app');
const chai = require('chai');
@@ -71,6 +72,7 @@ describe('Get moderation queues rejected, pending, flags', () => {
it('should return all the pending comments', function(done){
chai.request(app)
.get('/api/v1/queue/comments/pending')
.set(passport.inject({roles: ['admin']}))
.end(function(err, res){
expect(err).to.be.null;
expect(res).to.have.status(200);
+29 -25
View File
@@ -1,13 +1,15 @@
process.env.NODE_ENV = 'test';
require('../../../utils/mongoose');
const passport = require('../../../utils/passport');
const app = require('../../../../app');
const chai = require('chai');
const chaiHttp = require('chai-http');
chai.use(chaiHttp);
const expect = chai.expect;
chai.should();
chai.use(require('chai-http'));
const Setting = require('../../../../models/setting');
const defaults = {id: '1', moderation: 'pre'};
@@ -17,15 +19,16 @@ describe('GET /settings', () => {
return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
});
it('should return a settings object', done => {
chai.request(app)
it('should return a settings object', () => {
return chai.request(app)
.get('/api/v1/settings')
.end((err, res) => {
expect(err).to.be.null;
.set(passport.inject({
roles: ['admin']
}))
.then((res) => {
expect(res).to.have.status(200);
expect(res).to.be.json;
expect(res.body).to.have.property('moderation', 'pre');
done(err);
});
});
});
@@ -33,25 +36,26 @@ describe('GET /settings', () => {
// update the settings.
describe('update settings', () => {
it('should respond ok to a PUT', () => {
return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true})
.then(() => {
return chai.request(app)
.put('/api/v1/settings')
.send({moderation: 'post'})
.then(res => {
expect(res).to.have.status(204);
return Setting
.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true})
.then(() => {
return chai.request(app)
.put('/api/v1/settings')
.set(passport.inject({
roles: ['admin']
}))
.send({moderation: 'post'});
})
.then(res => {
expect(res).to.have.status(204);
return Setting.getSettings();
return Setting.getSettings();
})
.then(settings => {
})
.then(settings => {
// confirm updated settings in db
expect(settings).to.have.property('moderation');
expect(settings.moderation).to.equal('post');
})
.catch(err => {
throw err;
});
});
// confirm updated settings in db
expect(settings).to.have.property('moderation');
expect(settings.moderation).to.equal('post');
});
});
});
+25
View File
@@ -0,0 +1,25 @@
const authorization = require('../../middleware/authorization');
// Add the passport middleware here before it's setup.
authorization.middleware.push((req, res, next) => {
req.user = JSON.parse(new Buffer(req.get('X-Mock-Authorization'), 'base64').toString('ascii'));
next();
});
const MockStrategy = {
/**
* Injects the new user into the request header for the mock middleware to
* interpret.
* @param {Object} user the user to inject
* @return {Object} the headers to add to the request
*/
inject(user) {
return {
'X-Mock-Authorization': new Buffer(JSON.stringify(user)).toString('base64')
};
}
};
module.exports = MockStrategy;
+1 -1
View File
@@ -10,4 +10,4 @@
<div id="coralStream"></div>
<script src="/client/embed/stream/bundle.js"></script>
</body>
</html>
</html>