mirror of
https://github.com/wassname/talk.git
synced 2026-08-18 12:30:39 +08:00
Merge branch 'master' into guides
This commit is contained in:
@@ -5,5 +5,3 @@
|
||||
- click a button
|
||||
- view the cat
|
||||
- see the cat meow
|
||||
|
||||
@coralproject/tech
|
||||
|
||||
@@ -7,3 +7,4 @@ dist/coral-admin/bundle.js
|
||||
*.iml
|
||||
.env
|
||||
gaba.cfg
|
||||
.idea/
|
||||
|
||||
@@ -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 : {}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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)));
|
||||
};
|
||||
@@ -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>We’re sorry, but you don’t 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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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';
|
||||
+1
-1
@@ -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.'});
|
||||
+7
-4
@@ -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);
|
||||
|
||||
|
||||
+5
-1
@@ -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) {
|
||||
+11
-5
@@ -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')}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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')})
|
||||
})
|
||||
|
||||
@@ -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>;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)));
|
||||
};
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]) => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -10,4 +10,4 @@
|
||||
<div id="coralStream"></div>
|
||||
<script src="/client/embed/stream/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user