mirror of
https://github.com/wassname/talk.git
synced 2026-07-07 23:11:14 +08:00
Merge branch 'master' of https://github.com/coralproject/talk into stream-e2e
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_style = space
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
@@ -5,6 +5,7 @@ RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Setup the environment
|
||||
ENV NODE_ENV production
|
||||
ENV PATH /usr/src/app/bin:$PATH
|
||||
ENV TALK_PORT 5000
|
||||
EXPOSE 5000
|
||||
|
||||
@@ -14,6 +14,19 @@ Run it once to install the dependencies.
|
||||
`npm start`
|
||||
Runs Talk.
|
||||
|
||||
### Configuration
|
||||
|
||||
The Talk application requires specific configuration options to be available
|
||||
inside the environment in order to run, those variables are listed here:
|
||||
|
||||
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
|
||||
secure cookies
|
||||
- `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook
|
||||
Login enabled app.
|
||||
- `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your
|
||||
Facebook Login enabled app.
|
||||
- `TALK_ROOT_URL` (*required*) - Root url of the installed application externally available in the format: `<scheme>://<host>` without the path.
|
||||
|
||||
### Running with Docker
|
||||
Make sure you have Docker running first and then run `docker-compose up -d`
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@ const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
const helmet = require('helmet');
|
||||
const passport = require('./passport');
|
||||
const session = require('express-session');
|
||||
const RedisStore = require('connect-redis')(session);
|
||||
const redis = require('./redis');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -12,12 +17,62 @@ if (app.get('env') !== 'test') {
|
||||
app.use(morgan('dev'));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// APP MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
app.set('trust proxy', 'loopback');
|
||||
app.use(helmet());
|
||||
app.use(bodyParser.json());
|
||||
app.use('/client', express.static(path.join(__dirname, 'dist')));
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// Routes.
|
||||
app.use('/client', express.static(path.join(__dirname, 'dist')));
|
||||
//==============================================================================
|
||||
// SESSION MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
const session_opts = {
|
||||
secret: process.env.TALK_SESSION_SECRET,
|
||||
httpOnly: true,
|
||||
rolling: true,
|
||||
saveUninitialized: false,
|
||||
resave: false,
|
||||
name: 'talk.sid',
|
||||
cookie: {
|
||||
secure: false,
|
||||
maxAge: 18000000, // 30 minutes for expiry.
|
||||
},
|
||||
store: new RedisStore({
|
||||
ttl: 1800,
|
||||
client: redis,
|
||||
})
|
||||
};
|
||||
|
||||
if (app.get('env') === 'production') {
|
||||
|
||||
// Enable the secure cookie when we are in production mode.
|
||||
session_opts.cookie.secure = true;
|
||||
} else if (app.get('env') === 'test') {
|
||||
|
||||
// Add in the secret during tests.
|
||||
session_opts.secret = 'keyboard cat';
|
||||
}
|
||||
|
||||
app.use(session(session_opts));
|
||||
|
||||
//==============================================================================
|
||||
// PASSPORT MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
// Setup the PassportJS Middleware.
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
//==============================================================================
|
||||
// ROUTES
|
||||
//==============================================================================
|
||||
|
||||
app.use('/', require('./routes'));
|
||||
|
||||
//==============================================================================
|
||||
@@ -34,37 +89,27 @@ app.use((req, res, next) => {
|
||||
|
||||
// General error handler. Respond with the message and error if we have it while
|
||||
// returning a status code that makes sense.
|
||||
if (app.get('env') === 'development') {
|
||||
app.use('/api', (err, req, res, next) => {
|
||||
res.status(err.status || 500);
|
||||
res.json({
|
||||
message: err.message,
|
||||
error: err
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/', (err, req, res, next) => {
|
||||
res.status(err.status || 500);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: err
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.use('/api', (err, req, res, next) => {
|
||||
if (err !== ErrNotFound) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
res.status(err.status || 500);
|
||||
res.json({
|
||||
message: err.message,
|
||||
error: {}
|
||||
error: app.get('env') === 'development' ? err : null
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/', (err, req, res, next) => {
|
||||
if (err !== ErrNotFound) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
res.status(err.status || 500);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: {}
|
||||
error: app.get('env') === 'development' ? err : null
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -206,7 +206,7 @@ function listUsers() {
|
||||
const mongoose = require('../mongoose');
|
||||
|
||||
User
|
||||
.find()
|
||||
.all()
|
||||
.then((users) => {
|
||||
let table = new Table({
|
||||
head: [
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
const redis = require('./redis');
|
||||
|
||||
const cache = module.exports = {};
|
||||
|
||||
/**
|
||||
* This collects a key that may either be an array or a string and creates a
|
||||
* unified key out of it.
|
||||
* @param {Mixed} key Either an array of items composing a key or a string
|
||||
* @return {String} A string that represents a key
|
||||
*/
|
||||
const keyfunc = (key) => {
|
||||
if (Array.isArray(key)) {
|
||||
return `cache[${key.join(':')}]`;
|
||||
}
|
||||
|
||||
return `cache[${key}]`;
|
||||
};
|
||||
|
||||
/**
|
||||
* This wraps a complicated function with a cache, in the event that the item is
|
||||
* not inside the cache, it will perform the work to get it and then set it
|
||||
* followed by returning the value.
|
||||
* @param {Mixed} key Either an array of items or string represening this
|
||||
* work
|
||||
* @param {Integer} expiry Time in seconds for the cache entry to live for
|
||||
* @param {Function} work A function that returns a promise that can be
|
||||
* resolved as the value to cache.
|
||||
* @return {Promise} Resolves to the value either retrieved from cache
|
||||
*/
|
||||
cache.wrap = (key, expiry, work) => {
|
||||
return cache
|
||||
.get(key)
|
||||
.then((value) => {
|
||||
if (value !== null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return work()
|
||||
.then((value) => {
|
||||
return cache
|
||||
.set(key, value, expiry)
|
||||
.then(() => value);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This returns a promise that returns a promise that resolves with the value
|
||||
* from the cache or null if it does not exist in the cache.
|
||||
* @param {Mixed} key Either an array of items composing a key or a string
|
||||
* @return {Promise}
|
||||
*/
|
||||
cache.get = (key) => new Promise((resolve, reject) => {
|
||||
redis.get(keyfunc(key), (err, reply) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (reply !== null) {
|
||||
let value;
|
||||
|
||||
try {
|
||||
|
||||
// Parse the stored cache value from JSON.
|
||||
value = JSON.parse(reply);
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
|
||||
return resolve(value);
|
||||
}
|
||||
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This sets a value on the key with the expiry and then resolves once it is
|
||||
* done.
|
||||
* @param {Mixed} key Either an array of items composing a key or a string
|
||||
* @param {Mixed} value Object to be serialized and set to the cache
|
||||
* @param {Integer} expiry Time in seconds for the cache entry to live for
|
||||
* @return {Promise}
|
||||
*/
|
||||
cache.set = (key, value, expiry) => new Promise((resolve, reject) => {
|
||||
|
||||
// Serialize the value as JSON.
|
||||
let reply = JSON.stringify(value);
|
||||
|
||||
redis.set(keyfunc(key), reply, 'EX', expiry, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
FETCH_COMMENTERS_SUCCESS,
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
SORT_UPDATE,
|
||||
COMMENTERS_NEW_PAGE
|
||||
COMMENTERS_NEW_PAGE,
|
||||
SET_ROLE
|
||||
} from '../constants/community';
|
||||
|
||||
import {base, getInit, handleResp} from '../helpers/response';
|
||||
@@ -40,3 +41,9 @@ export const newPage = () => ({
|
||||
type: COMMENTERS_NEW_PAGE
|
||||
});
|
||||
|
||||
export const setRole = (id, role) => dispatch => {
|
||||
return fetch(`${base}/user/${id}/role`, getInit('POST', {role}))
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,34 +3,49 @@ 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/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
import Linkify from 'react-linkify';
|
||||
|
||||
const linkify = new Linkify();
|
||||
|
||||
// Render a single comment for the list
|
||||
export default props => (
|
||||
<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>
|
||||
<span>{props.comment.get('name') || lang.t('comment.anon')}</span>
|
||||
<span className={styles.created}>{timeago().format(props.comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
|
||||
{props.comment.get('flagged') ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
|
||||
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 : ''}`}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<i className={`material-icons ${styles.avatar}`}>person</i>
|
||||
<span>{props.comment.get('name') || lang.t('comment.anon')}</span>
|
||||
<span className={styles.created}>{timeago().format(props.comment.get('createdAt') || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
|
||||
{props.comment.get('flagged') ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
|
||||
</div>
|
||||
<div>
|
||||
{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}
|
||||
onClick={() => props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))}
|
||||
fab colored>
|
||||
<Icon name={props.actionsMap[action].icon} />
|
||||
</Button>
|
||||
) : null)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
{props.actions.map(action => canShowAction(action, props.comment) ? (
|
||||
<Button className={styles.actionButton}
|
||||
onClick={() => props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))}
|
||||
fab colored>
|
||||
<Icon name={props.actionsMap[action].icon} />
|
||||
</Button>
|
||||
) : null)}
|
||||
<div className={styles.itemBody}>
|
||||
<span className={styles.body}>
|
||||
<Linkify component='span' properties={{style: linkStyles}}>
|
||||
{props.comment.get('body')}
|
||||
</Linkify>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.itemBody}>
|
||||
<span className={styles.body}>{props.comment.get('body')}</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
// Check if an action can be performed over a comment
|
||||
const canShowAction = (action, comment) => {
|
||||
@@ -43,4 +58,9 @@ const canShowAction = (action, comment) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const linkStyles = {
|
||||
backgroundColor: 'rgb(255, 219, 135)',
|
||||
padding: '1px 2px'
|
||||
};
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -121,3 +121,15 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,26 @@ 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/'}>Moderate</Link>
|
||||
<Link className={styles.navLink} to={'/admin/configure'}>Configure</Link>
|
||||
<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/'}>Moderate</Link>
|
||||
<Link className={styles.navLink} to={'/admin/configure'}>Configure</Link>
|
||||
<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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import styles from './ModerationKeysModal.css';
|
||||
|
||||
@@ -2,13 +2,17 @@ import React from 'react';
|
||||
import {Navigation, Drawer} 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';
|
||||
|
||||
export default () => (
|
||||
<Drawer>
|
||||
<Navigation>
|
||||
<Link className={styles.navLink} to="/admin">Moderate</Link>
|
||||
<Link className={styles.navLink} to="/admin/community">Community</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure">Configure</Link>
|
||||
<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>
|
||||
</Navigation>
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -2,16 +2,20 @@ import React from 'react';
|
||||
import {Navigation, 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';
|
||||
|
||||
export default () => (
|
||||
<Header title='Talk'>
|
||||
<Navigation>
|
||||
<Link className={styles.navLink} to="/admin">Moderate</Link>
|
||||
<Link className={styles.navLink} to="/admin/community">Community</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure">Configure</Link>
|
||||
<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>
|
||||
</Navigation>
|
||||
</Header>
|
||||
);
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -3,3 +3,4 @@ export const FETCH_COMMENTERS_SUCCESS = 'FETCH_COMMENTERS_SUCCESS';
|
||||
export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE';
|
||||
export const SORT_UPDATE = 'SORT_UPDATE';
|
||||
export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE';
|
||||
export const SET_ROLE = 'SET_ROLE';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../../translations';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import {Grid, Cell} from 'react-mdl';
|
||||
|
||||
import styles from './Community.css';
|
||||
@@ -19,6 +19,10 @@ const tableHeaders = [
|
||||
{
|
||||
title: lang.t('community.account_creation_date'),
|
||||
field: 'created_at'
|
||||
},
|
||||
{
|
||||
title: lang.t('community.newsroom_role'),
|
||||
field: 'role'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,34 +1,66 @@
|
||||
import React from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import styles from './Community.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations';
|
||||
import {setRole} from '../../actions/community';
|
||||
|
||||
const Table = ({headers, data, onHeaderClickHandler}) => (
|
||||
<table className={`mdl-data-table ${styles.dataTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, i) =>(
|
||||
<th
|
||||
key={i}
|
||||
className="mdl-data-table__cell--non-numeric"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, i)=> (
|
||||
<tr key={i}>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.displayName}
|
||||
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.created_at}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default Table;
|
||||
class Table extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.onRoleChange = this.onRoleChange.bind(this);
|
||||
}
|
||||
|
||||
onRoleChange (id, role) {
|
||||
this.props.dispatch(setRole(id, role));
|
||||
}
|
||||
|
||||
render () {
|
||||
const {headers, commenters, onHeaderClickHandler} = this.props;
|
||||
|
||||
return (
|
||||
<table className={`mdl-data-table ${styles.dataTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, i) =>(
|
||||
<th
|
||||
key={i}
|
||||
className="mdl-data-table__cell--non-numeric"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{commenters.map((row, i)=> (
|
||||
<tr key={i}>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.displayName}
|
||||
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.created_at}
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField label={'Select me'} value={row.roles[0] || ''}
|
||||
label={lang.t('community.role')}
|
||||
onChange={role => this.onRoleChange(row.id, role)}>
|
||||
<Option value={''}>.</Option>
|
||||
<Option value={'moderator'}>{lang.t('community.moderator')}</Option>
|
||||
<Option value={'admin'}>{lang.t('community.admin')}</Option>
|
||||
</SelectField>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({commenters: state.community.get('commenters')}))(Table);
|
||||
|
||||
@@ -23,6 +23,21 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.configSettingInfoBox {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
width: auto;
|
||||
height: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.configSettingInfoBox p {
|
||||
font-size: 12px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.configSettingEmbed {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
@@ -53,3 +68,7 @@
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@ import {
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
ListItemAction,
|
||||
//Textfield,
|
||||
Textfield,
|
||||
Checkbox,
|
||||
Button,
|
||||
Icon
|
||||
} from 'react-mdl';
|
||||
import styles from './Configure.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
class Configure extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -24,6 +24,8 @@ class Configure extends React.Component {
|
||||
|
||||
this.copyToClipBoard = this.copyToClipBoard.bind(this);
|
||||
this.updateModeration = this.updateModeration.bind(this);
|
||||
this.updateInfoBoxEnable = this.updateInfoBoxEnable.bind(this);
|
||||
this.updateInfoBoxContent = this.updateInfoBoxContent.bind(this);
|
||||
this.saveSettings = this.saveSettings.bind(this);
|
||||
}
|
||||
|
||||
@@ -36,6 +38,16 @@ class Configure extends React.Component {
|
||||
this.props.dispatch(updateSettings({moderation}));
|
||||
}
|
||||
|
||||
updateInfoBoxEnable () {
|
||||
const infoBoxEnable = !this.props.settings.infoBoxEnable;
|
||||
this.props.dispatch(updateSettings({infoBoxEnable}));
|
||||
}
|
||||
|
||||
updateInfoBoxContent (event) {
|
||||
const infoBoxContent = event.target.value;
|
||||
this.props.dispatch(updateSettings({infoBoxContent}));
|
||||
}
|
||||
|
||||
saveSettings () {
|
||||
this.props.dispatch(saveSettingsToServer());
|
||||
}
|
||||
@@ -48,22 +60,30 @@ class Configure extends React.Component {
|
||||
onClick={this.updateModeration}
|
||||
checked={this.props.settings.moderation === 'pre'} />
|
||||
</ListItemAction>
|
||||
Enable pre-moderation
|
||||
{lang.t('configure.enable-pre-moderation')}
|
||||
</ListItem>
|
||||
{/*
|
||||
<ListItem className={styles.configSetting}>
|
||||
<ListItemAction><Checkbox /></ListItemAction>
|
||||
Include Comment Stream Description for Readers
|
||||
<ListItem threeLine className={styles.configSettingInfoBox}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={this.updateInfoBoxEnable}
|
||||
checked={this.props.settings.infoBoxEnable} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.include-comment-stream')}
|
||||
<p>
|
||||
{lang.t('configure.include-comment-stream-desc')}
|
||||
</p>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.configSetting}>
|
||||
<ListItemAction><Checkbox /></ListItemAction>
|
||||
Limit Comment Length
|
||||
<Textfield
|
||||
pattern='-?[0-9]*(\.[0-9]+)?'
|
||||
error='Input is not a number!'
|
||||
label='Maximum Characters' />
|
||||
<ListItem className={`${styles.configSettingInfoBox} ${this.props.settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<ListItemContent>
|
||||
<Textfield
|
||||
onChange={this.updateInfoBoxContent}
|
||||
value={this.props.settings.infoBoxContent}
|
||||
label={lang.t('configure.include-text')}
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
*/}
|
||||
</List>;
|
||||
}
|
||||
|
||||
@@ -84,7 +104,7 @@ class Configure extends React.Component {
|
||||
|
||||
return <List>
|
||||
<ListItem className={styles.configSettingEmbed}>
|
||||
<p>Copy and paste code below into your CMS to embed your comment box in your articles</p>
|
||||
<p>{lang.t('configure.copy-and-paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<Button raised colored className={styles.copyButton} onClick={this.copyToClipBoard}>
|
||||
{lang.t('embedlink.copy')}
|
||||
@@ -100,8 +120,8 @@ class Configure extends React.Component {
|
||||
|
||||
render () {
|
||||
let pageTitle = this.state.activeSection === 'comments'
|
||||
? 'Comment Settings'
|
||||
: 'Embed Comment Stream';
|
||||
? lang.t('configure.comment-settings')
|
||||
: lang.t('configure.embed-comment-stream');
|
||||
|
||||
if (this.props.fetchingSettings) {
|
||||
pageTitle += ' - Loading...';
|
||||
@@ -114,16 +134,16 @@ class Configure extends React.Component {
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection.bind(this, 'comments')}
|
||||
icon='settings'>Comment Settings</ListItemContent>
|
||||
icon='settings'>{lang.t('configure.comment-settings')}</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection.bind(this, 'embed')}
|
||||
icon='code'>Embed Comment Stream</ListItemContent>
|
||||
icon='code'>{lang.t('configure.embed-comment-stream')}</ListItemContent>
|
||||
</ListItem>
|
||||
</List>
|
||||
<Button raised colored onClick={this.saveSettings}>
|
||||
<Icon name='save' /> Save Changes
|
||||
<Icon name='save' /> {lang.t('configure.save-changes')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
|
||||
@@ -5,8 +5,8 @@ import CommentList from 'components/CommentList';
|
||||
import {updateStatus} from 'actions/comments';
|
||||
import styles from './ModerationQueue.css';
|
||||
import key from 'keymaster';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
/*
|
||||
* Renders the moderation queue as a tabbed layout with 3 moderation
|
||||
@@ -91,7 +91,7 @@ class ModerationQueue extends React.Component {
|
||||
commentIds={
|
||||
comments
|
||||
.get('ids')
|
||||
.filter(id =>
|
||||
.filter(id =>
|
||||
comments
|
||||
.get('byId')
|
||||
.get(id)
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
FETCH_COMMENTERS_REQUEST,
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
FETCH_COMMENTERS_SUCCESS,
|
||||
SORT_UPDATE
|
||||
SORT_UPDATE,
|
||||
SET_ROLE
|
||||
} from '../constants/community';
|
||||
|
||||
const initialState = Map({
|
||||
@@ -37,6 +38,13 @@ export default function community (state = initialState, action) {
|
||||
})
|
||||
.set('commenters', commenters); // Sets to normal array
|
||||
}
|
||||
case SET_ROLE : {
|
||||
const commenters = state.get('commenters');
|
||||
const idx = commenters.findIndex(el => el.id === action.id);
|
||||
|
||||
commenters[idx].roles[0] = action.role;
|
||||
return state.set('commenters', commenters.map(id => id));
|
||||
}
|
||||
case SORT_UPDATE :
|
||||
return state
|
||||
.set('field', action.sort.field)
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
export default {
|
||||
en: {
|
||||
'community': {
|
||||
username_and_email: 'Username and Email',
|
||||
account_creation_date: 'Account Creation Date'
|
||||
},
|
||||
'modqueue': {
|
||||
'pending': 'pending',
|
||||
'rejected': 'rejected',
|
||||
'flagged': 'flagged',
|
||||
'shortcuts': 'Shortcuts',
|
||||
'close': 'Close',
|
||||
'actions': 'Actions',
|
||||
'navigation': 'Navigation',
|
||||
'approve': 'Approve comment',
|
||||
'reject': 'Reject comment',
|
||||
'nextcomment': 'Go to the next comment',
|
||||
'prevcomment': 'Go to the previous comment',
|
||||
'singleview': 'Toggle single comment edit view',
|
||||
'thismenu': 'Open this menu'
|
||||
},
|
||||
'comment': {
|
||||
'flagged': 'flagged',
|
||||
'anon': 'Anonymous'
|
||||
},
|
||||
'embedlink': {
|
||||
'copy': 'Copy to Clipboard'
|
||||
}
|
||||
},
|
||||
es: {
|
||||
'community': {
|
||||
username_and_email: 'Usuario y E-mail',
|
||||
account_creation_date: 'Fecha de creación de la cuenta'
|
||||
},
|
||||
'modqueue': {
|
||||
'pending': 'pendiente',
|
||||
'rejected': 'rechazado',
|
||||
'flagged': 'marcado',
|
||||
'shortcuts': 'Atajos de teclado',
|
||||
'close': 'Cerrar'
|
||||
},
|
||||
'comment': {
|
||||
'flagged': 'marcado',
|
||||
'anon': 'Anónimo'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"en": {
|
||||
"community": {
|
||||
"username_and_email": "Username and Email",
|
||||
"account_creation_date": "Account Creation Date",
|
||||
"newsroom_role": "Newsroom Role",
|
||||
"admin": "Administrator",
|
||||
"moderator": "Moderator",
|
||||
"role": "Select role..."
|
||||
},
|
||||
"modqueue": {
|
||||
"pending": "pending",
|
||||
"rejected": "rejected",
|
||||
"flagged": "flagged",
|
||||
"shortcuts": "Shortcuts",
|
||||
"close": "Close",
|
||||
"actions": "Actions",
|
||||
"navigation": "Navigation",
|
||||
"approve": "Approve comment",
|
||||
"reject": "Reject comment",
|
||||
"nextcomment": "Go to the next comment",
|
||||
"prevcomment": "Go to the previous comment",
|
||||
"singleview": "Toggle single comment edit view",
|
||||
"thismenu": "Open this menu"
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "flagged",
|
||||
"anon": "Anonymous"
|
||||
},
|
||||
"embedlink": {
|
||||
"copy": "Copy to Clipboard"
|
||||
},
|
||||
"configure": {
|
||||
"enable-pre-moderation": "Enable pre-moderation",
|
||||
"include-comment-stream": "Include Comment Stream Description for Readers.",
|
||||
"include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.",
|
||||
"include-text": "Include your text here.",
|
||||
"comment-settings": "Comment Settings",
|
||||
"embed-comment-stream": "Embed Comment Stream",
|
||||
"save-changes": "Save Changes",
|
||||
"copy-and-paste": "Copy and paste code below into your CMS to embed your comment box in your articles",
|
||||
"moderate": "Moderate",
|
||||
"configure": "Configure",
|
||||
"community": "Community"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"community": {
|
||||
"username_and_email": "Usuario y E-mail",
|
||||
"account_creation_date": "Fecha de creación de la cuenta",
|
||||
"newsroom_role": "Rol en la redacción",
|
||||
"admin": "Administrador",
|
||||
"moderator": "Moderador",
|
||||
"role": "Select role..."
|
||||
},
|
||||
"modqueue": {
|
||||
"pending": "pendiente",
|
||||
"rejected": "rechazado",
|
||||
"flagged": "marcado",
|
||||
"shortcuts": "Atajos de teclado",
|
||||
"close": "Cerrar"
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "marcado",
|
||||
"anon": "Anónimo"
|
||||
},
|
||||
"configure": {
|
||||
"enable-pre-moderation": "Habilitar pre-moderación",
|
||||
"include-comment-stream": "Incluir la Descripción a un Hilo de Comentario para los y las Lectoras.",
|
||||
"include-comment-stream-desc": "Escribir un mensaje que será agregado a la parte de arriba del tu hilo de comentarios. Por ejemplo, un tema, guias de comunidad, etc.",
|
||||
"include-text": "Incluir tu texto aqui.",
|
||||
"comment-settings": "Configuración de Comentarios",
|
||||
"embed-comment-stream": "Colocar Hilo de Comentarios",
|
||||
"save-changes": "Guardar Cambios",
|
||||
"copy-and-paste": "Copiar y pegar el código de más abajo en tu CMS para colocar la caja de comentarios en tus articulos",
|
||||
"moderate": "Moderar",
|
||||
"configure": "Configurar",
|
||||
"community": "Comunidad"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '../../coral-framework';
|
||||
import {connect} from 'react-redux';
|
||||
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
|
||||
import InfoBox from '../../coral-plugin-infobox/InfoBox';
|
||||
import Content from '../../coral-plugin-commentcontent/CommentContent';
|
||||
import PubDate from '../../coral-plugin-pubdate/PubDate';
|
||||
import Count from '../../coral-plugin-comment-count/CommentCount';
|
||||
@@ -14,11 +15,14 @@ import AuthorName from '../../coral-plugin-author-name/AuthorName';
|
||||
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies';
|
||||
import Pym from 'pym.js';
|
||||
import FlagButton from '../../coral-plugin-flags/FlagButton';
|
||||
import LikeButton from '../../coral-plugin-likes/LikeButton';
|
||||
import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton';
|
||||
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
|
||||
import UserBox from '../../coral-sign-in/components/UserBox';
|
||||
|
||||
const {addItem, updateItem, postItem, getStream, postAction, appendItemArray} = itemActions;
|
||||
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
|
||||
const {addNotification, clearNotification} = notificationActions;
|
||||
const {setLoggedInUser} = authActions;
|
||||
const {logout} = authActions;
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
@@ -29,37 +33,21 @@ const mapStateToProps = (state) => {
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
addItem: (item, itemType) => {
|
||||
return dispatch(addItem(item, itemType));
|
||||
},
|
||||
updateItem: (id, property, value, itemType) => {
|
||||
return dispatch(updateItem(id, property, value, itemType));
|
||||
},
|
||||
postItem: (data, type, id) => {
|
||||
return dispatch(postItem(data, type, id));
|
||||
},
|
||||
getStream: (rootId) => {
|
||||
return dispatch(getStream(rootId));
|
||||
},
|
||||
addNotification: (type, text) => {
|
||||
return dispatch(addNotification(type, text));
|
||||
},
|
||||
clearNotification: () => {
|
||||
return dispatch(clearNotification());
|
||||
},
|
||||
setLoggedInUser: (user_id) => {
|
||||
return dispatch(setLoggedInUser(user_id));
|
||||
},
|
||||
postAction: (item, action, user, itemType) => {
|
||||
return dispatch(postAction(item, action, user, itemType));
|
||||
},
|
||||
appendItemArray: (item, property, value, addToFront, itemType) => {
|
||||
return dispatch(appendItemArray(item, property, value, addToFront, itemType));
|
||||
}
|
||||
};
|
||||
};
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
addItem: (item, itemType) => dispatch(addItem(item, itemType)),
|
||||
updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)),
|
||||
postItem: (data, type, id) => dispatch(postItem(data, type, id)),
|
||||
getStream: (rootId) => dispatch(getStream(rootId)),
|
||||
addNotification: (type, text) => dispatch(addNotification(type, text)),
|
||||
clearNotification: () => dispatch(clearNotification()),
|
||||
postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)),
|
||||
deleteAction: (item, action, user, itemType) => {
|
||||
return dispatch(deleteAction(item, action, user, itemType));
|
||||
},
|
||||
appendItemArray: (item, property, value, addToFront, itemType) =>
|
||||
dispatch(appendItemArray(item, property, value, addToFront, itemType)),
|
||||
logout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
class CommentStream extends Component {
|
||||
|
||||
@@ -72,8 +60,9 @@ class CommentStream extends Component {
|
||||
componentDidMount () {
|
||||
// Set up messaging between embedded Iframe an parent component
|
||||
// Using recommended Pym init code which violates .eslint standards
|
||||
new Pym.Child({polling: 500});
|
||||
this.props.getStream('assetTest');
|
||||
const pym = new Pym.Child({polling: 100});
|
||||
const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl)[1];
|
||||
this.props.getStream(path);
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -84,25 +73,30 @@ class CommentStream extends Component {
|
||||
url: 'http://coralproject.net'
|
||||
}, 'asset', 'assetTest');
|
||||
|
||||
// Loading mock user
|
||||
this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989')
|
||||
.then((id) => {
|
||||
this.props.setLoggedInUser(id);
|
||||
});
|
||||
// Loading mock user
|
||||
//this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989')
|
||||
// .then((id) => {
|
||||
// this.props.setLoggedInUser(id);
|
||||
// });
|
||||
}
|
||||
|
||||
// TODO: Replace teststream id with id from params
|
||||
|
||||
const rootItemId = 'assetTest';
|
||||
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>
|
||||
{
|
||||
rootItem
|
||||
? <div>
|
||||
<div id="commentBox">
|
||||
<InfoBox
|
||||
content={this.props.config.infoBoxContent}
|
||||
enable={this.props.config.infoBoxEnable}/>
|
||||
<Count
|
||||
id={rootItemId}
|
||||
items={this.props.items}/>
|
||||
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
|
||||
<CommentBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
@@ -110,31 +104,46 @@ class CommentStream extends Component {
|
||||
updateItem={this.props.updateItem}
|
||||
id={rootItemId}
|
||||
premod={this.props.config.moderation}
|
||||
reply={false}/>
|
||||
reply={false}
|
||||
canPost={loggedIn}
|
||||
/>
|
||||
{!loggedIn && <SignInContainer />}
|
||||
</div>
|
||||
{
|
||||
rootItem.comments.map((commentId) => {
|
||||
rootItem.comments && rootItem.comments.map((commentId) => {
|
||||
const comment = this.props.items.comments[commentId];
|
||||
return <div className="comment" key={commentId}>
|
||||
<hr aria-hidden={true}/>
|
||||
<AuthorName name={comment.username}/>
|
||||
<PubDate created_at={comment.created_at}/>
|
||||
<Content body={comment.body}/>
|
||||
<div className="commentActions">
|
||||
<div className="commentActionsLeft">
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={commentId}/>
|
||||
<LikeButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
like={this.props.items.actions[comment.like]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
</div>
|
||||
<div className="commentActionsRight">
|
||||
<FlagButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
flag={this.props.items.actions[comment.flag]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={commentId}/>
|
||||
<PermalinkButton
|
||||
comment_id={commentId}
|
||||
asset_id={comment.asset_id}/>
|
||||
<PermalinkButton
|
||||
comment_id={commentId}
|
||||
asset_id={comment.asset_id}/>
|
||||
</div>
|
||||
<ReplyBox
|
||||
addNotification={this.props.addNotification}
|
||||
@@ -154,23 +163,35 @@ class CommentStream extends Component {
|
||||
<AuthorName name={reply.username}/>
|
||||
<PubDate created_at={reply.created_at}/>
|
||||
<Content body={reply.body}/>
|
||||
<div className="replyActions">
|
||||
<FlagButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
flag={this.props.items.actions[reply.flag]}
|
||||
postAction={this.props.postAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
parent_id={reply.parent_id}/>
|
||||
<PermalinkButton
|
||||
comment_id={reply.comment_id}
|
||||
asset_id={reply.comment_id}
|
||||
/>
|
||||
</div>
|
||||
<div className="replyActionsLeft">
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={replyId}/>
|
||||
<LikeButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
like={this.props.items.actions[reply.like]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
</div>
|
||||
<div className="replyActionsRight">
|
||||
<FlagButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
flag={this.props.items.actions[reply.flag]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<PermalinkButton
|
||||
comment_id={reply.comment_id}
|
||||
asset_id={reply.comment_id}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ body {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
min-height: 700px;
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -49,6 +50,23 @@ hr {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Info Box Styles */
|
||||
.coral-plugin-infobox-info {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
border: 0;
|
||||
background: rgb(105,105,105);
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Comment Box Styles */
|
||||
.coral-plugin-commentbox-container {
|
||||
display: flex;
|
||||
@@ -105,16 +123,33 @@ hr {
|
||||
|
||||
/* Comment Action Styles */
|
||||
|
||||
.commentActions, .replyActions {
|
||||
.commentActionsRight, .replyActionsRight {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 50%;
|
||||
}
|
||||
.commentActionsLeft, .replyActionsLeft {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.commentActions .material-icons, .replyActions .material-icons {
|
||||
.commentActionsLeft .material-icons,.commentActionsRight .material-icons,
|
||||
.replyActionsLeft .material-icons, .replyActionsRight .material-icons {
|
||||
font-size: 12px;
|
||||
margin-left: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.likedButton {
|
||||
color: rgb(0,134,227);
|
||||
}
|
||||
|
||||
.flaggedIcon {
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
/* Comment count styles */
|
||||
.coral-plugin-comment-count-text {
|
||||
margin-bottom: 15px;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from './../translations';
|
||||
const lang = new I18n(translations);
|
||||
import * as actions from '../constants/auth';
|
||||
import {base, handleResp, getInit} from '../helpers/response';
|
||||
|
||||
// Dialog Actions
|
||||
export const showSignInDialog = () => ({type: actions.SHOW_SIGNIN_DIALOG});
|
||||
export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG});
|
||||
|
||||
export const changeView = view => dispatch =>
|
||||
dispatch({
|
||||
type: actions.CHANGE_VIEW,
|
||||
view
|
||||
});
|
||||
|
||||
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});
|
||||
|
||||
export const fetchSignIn = (formData) => dispatch => {
|
||||
dispatch(signInRequest());
|
||||
fetch(`${base}/auth/local`, getInit('POST', formData))
|
||||
.then(handleResp)
|
||||
.then(({user}) => {
|
||||
dispatch(hideSignInDialog());
|
||||
dispatch(signInSuccess(user));
|
||||
})
|
||||
.catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError'))));
|
||||
};
|
||||
|
||||
// Sign In - Facebook
|
||||
|
||||
const signInFacebookRequest = () => ({type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST});
|
||||
const signInFacebookSuccess = user => ({type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS, user});
|
||||
const signInFacebookFailure = error => ({type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE, error});
|
||||
|
||||
export const fetchSignInFacebook = () => dispatch => {
|
||||
dispatch(signInFacebookRequest());
|
||||
window.open(
|
||||
`${base}/auth/facebook`,
|
||||
'Continue with Facebook',
|
||||
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
|
||||
);
|
||||
};
|
||||
|
||||
export const facebookCallback = (err, data) => dispatch => {
|
||||
if (err) {
|
||||
signInFacebookFailure(err);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
dispatch(signInFacebookSuccess(JSON.parse(data)));
|
||||
dispatch(hideSignInDialog());
|
||||
} catch (err) {
|
||||
dispatch(signInFacebookFailure(err));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Sign Up Actions
|
||||
|
||||
const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST});
|
||||
const signUpSuccess = user => ({type: actions.FETCH_SIGNUP_SUCCESS, user});
|
||||
const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error});
|
||||
|
||||
export const fetchSignUp = formData => dispatch => {
|
||||
dispatch(signUpRequest());
|
||||
fetch(`${base}/user`, getInit('POST', formData))
|
||||
.then(handleResp)
|
||||
.then(({user}) => {
|
||||
dispatch(signUpSuccess(user));
|
||||
setTimeout(() =>{
|
||||
dispatch(changeView('SIGNIN'));
|
||||
}, 3000);
|
||||
})
|
||||
.catch(() => dispatch(signUpFailure(lang.t('error.emailInUse')))); // We need to inprove error handling. TODO (bc)
|
||||
};
|
||||
|
||||
// Forgot Password Actions
|
||||
|
||||
const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUEST});
|
||||
const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS});
|
||||
const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE});
|
||||
|
||||
export const fetchForgotPassword = () => dispatch => {
|
||||
dispatch(forgotPassowordRequest());
|
||||
fetch(`${base}/user/request-password-reset`, getInit('POST'))
|
||||
.then(handleResp)
|
||||
.then(() => dispatch(forgotPassowordSuccess()))
|
||||
.catch(error => dispatch(forgotPassowordFailure(error)));
|
||||
};
|
||||
|
||||
// LogOut Actions
|
||||
|
||||
const logOutRequest = () => ({type: actions.LOGOUT_REQUEST});
|
||||
const logOutSuccess = () => ({type: actions.LOGOUT_SUCCESS});
|
||||
const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
|
||||
|
||||
export const logout = () => dispatch => {
|
||||
dispatch(logOutRequest());
|
||||
fetch(`${base}/auth`, getInit('DELETE'))
|
||||
.then(handleResp)
|
||||
.then(() => dispatch(logOutSuccess()))
|
||||
.catch(error => dispatch(logOutFailure(error)));
|
||||
};
|
||||
|
||||
// LogOut Actions
|
||||
|
||||
export const validForm = () => ({type: actions.VALID_FORM});
|
||||
export const invalidForm = error => ({type: actions.INVALID_FORM, error});
|
||||
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
/* @flow */
|
||||
|
||||
import {fromJS} from 'immutable';
|
||||
|
||||
/**
|
||||
+56
-49
@@ -8,6 +8,23 @@ export const ADD_ITEM = 'ADD_ITEM';
|
||||
export const UPDATE_ITEM = 'UPDATE_ITEM';
|
||||
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY';
|
||||
|
||||
const getInit = (method, body) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
const init = {method, headers};
|
||||
if (method.toLowerCase() !== 'get') {
|
||||
init.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return init;
|
||||
};
|
||||
|
||||
const responseHandler = response => {
|
||||
return response.ok ? response.json() : Promise.reject(`${response.status} ${response.statusText}`);
|
||||
};
|
||||
/**
|
||||
* Action creators
|
||||
*/
|
||||
@@ -77,14 +94,10 @@ export const appendItemArray = (id, property, value, add_to_front, item_type) =>
|
||||
* @dispatches
|
||||
* A set of items to the item store
|
||||
*/
|
||||
export function getStream (assetId) {
|
||||
export function getStream (assetUrl) {
|
||||
return (dispatch) => {
|
||||
return fetch(`/api/v1/stream?asset_id=${assetId}`)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json() : Promise.reject(`${response.status} ${response.statusText}`);
|
||||
}
|
||||
)
|
||||
return fetch(`/api/v1/stream?asset_url=${encodeURIComponent(assetUrl)}`)
|
||||
.then(responseHandler)
|
||||
.then((json) => {
|
||||
|
||||
/* Add items to the store */
|
||||
@@ -95,6 +108,8 @@ export function getStream (assetId) {
|
||||
}
|
||||
}
|
||||
|
||||
const assetId = json.assets[0].id;
|
||||
|
||||
/* Sort comments by date*/
|
||||
json.comments.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
const rels = json.comments.reduce((h, item) => {
|
||||
@@ -112,10 +127,7 @@ export function getStream (assetId) {
|
||||
return h;
|
||||
}, {rootComments: [], childComments: {}});
|
||||
|
||||
dispatch(addItem({
|
||||
id: assetId,
|
||||
comments: rels.rootComments,
|
||||
}, 'assets'));
|
||||
dispatch(updateItem(assetId, 'comments', rels.rootComments, 'assets'));
|
||||
|
||||
const childKeys = Object.keys(rels.childComments);
|
||||
for (let i = 0; i < childKeys.length; i++ ) {
|
||||
@@ -148,13 +160,8 @@ export function getStream (assetId) {
|
||||
|
||||
export function getItemsArray (ids) {
|
||||
return (dispatch) => {
|
||||
return fetch(`/v1/item/${ids}`)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json()
|
||||
: Promise.reject(`${response.status } ${ response.statusText}`);
|
||||
}
|
||||
)
|
||||
return fetch(`/v1/item/${ids}`, getInit('GET'))
|
||||
.then(responseHandler)
|
||||
.then((json) => {
|
||||
for (let i = 0; i < json.items.length; i++) {
|
||||
dispatch(addItem(json.items[i]));
|
||||
@@ -183,20 +190,8 @@ export function postItem (item, type, id) {
|
||||
if (id) {
|
||||
item.id = id;
|
||||
}
|
||||
let options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(item),
|
||||
headers: {
|
||||
'Content-Type':'application/json'
|
||||
}
|
||||
};
|
||||
return fetch(`/api/v1/${type}`, options)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json()
|
||||
: Promise.reject(`${response.status} ${response.statusText}`);
|
||||
}
|
||||
)
|
||||
return fetch(`/api/v1/${type}`, getInit('POST', item))
|
||||
.then(responseHandler)
|
||||
.then((json) => {
|
||||
dispatch(addItem({...item, id:json.id}, type));
|
||||
return json.id;
|
||||
@@ -227,23 +222,35 @@ export function postAction (item_id, action_type, user_id, item_type) {
|
||||
action_type,
|
||||
user_id
|
||||
};
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type':'application/json'
|
||||
},
|
||||
body: JSON.stringify(action)
|
||||
};
|
||||
|
||||
return fetch(`/api/v1/${item_type}/${item_id}/actions`, options)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json()
|
||||
: Promise.reject(`${response.status} ${response.statusText}`);
|
||||
}
|
||||
)
|
||||
.then((json)=>{
|
||||
return json;
|
||||
});
|
||||
return fetch(`/api/v1/${item_type}/${item_id}/actions`, getInit('POST', action))
|
||||
.then(responseHandler);
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* DeleteAction
|
||||
* Deletes an action to an item
|
||||
*
|
||||
* @params
|
||||
* id - the id of the item on which the action is taking place
|
||||
* action - the name of the action
|
||||
* user - the user performing the action
|
||||
* host - the coral host
|
||||
*
|
||||
* @returns
|
||||
* A promise resolving to null or an error
|
||||
*
|
||||
*/
|
||||
|
||||
export function deleteAction (item_id, action_type, user_id, item_type) {
|
||||
return () => {
|
||||
const action = {
|
||||
action_type,
|
||||
user_id
|
||||
};
|
||||
|
||||
return fetch(`/api/v1/${item_type}/${item_id}/actions`, getInit('DELETE', action))
|
||||
.then(responseHandler);
|
||||
};
|
||||
}
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
/* Notification Actions */
|
||||
|
||||
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION';
|
||||
export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
export const CHANGE_VIEW = 'CHANGE_VIEW';
|
||||
export const CLEAN_STATE = 'CLEAN_STATE';
|
||||
|
||||
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
|
||||
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
|
||||
|
||||
export const FETCH_SIGNUP_REQUEST = 'FETCH_SIGNUP_REQUEST';
|
||||
export const FETCH_SIGNUP_FAILURE = 'FETCH_SIGNUP_FAILURE';
|
||||
export const FETCH_SIGNUP_SUCCESS = 'FETCH_SIGNUP_SUCCESS';
|
||||
|
||||
export const FETCH_SIGNIN_REQUEST = 'FETCH_SIGNIN_REQUEST';
|
||||
export const FETCH_SIGNIN_FAILURE = 'FETCH_SIGNIN_FAILURE';
|
||||
export const FETCH_SIGNIN_SUCCESS = 'FETCH_SIGNIN_SUCCESS';
|
||||
|
||||
export const FETCH_SIGNIN_FACEBOOK_REQUEST = 'FETCH_SIGNIN_FACEBOOK_REQUEST';
|
||||
export const FETCH_SIGNIN_FACEBOOK_FAILURE = 'FETCH_SIGNIN_FACEBOOK_FAILURE';
|
||||
export const FETCH_SIGNIN_FACEBOOK_SUCCESS = 'FETCH_SIGNIN_FACEBOOK_SUCCESS';
|
||||
|
||||
export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
|
||||
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
|
||||
export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE';
|
||||
|
||||
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
|
||||
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
|
||||
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
|
||||
|
||||
export const INVALID_FORM = 'INVALID_FORM';
|
||||
export const VALID_FORM = 'VALID_FORM';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from './../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default {
|
||||
email: lang.t('error.email'),
|
||||
password: lang.t('error.password'),
|
||||
displayName: lang.t('error.displayName'),
|
||||
confirmPassword: lang.t('error.confirmPassword')
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
export const base = '/api/v1';
|
||||
|
||||
export const getInit = (method, body) => {
|
||||
let init = {
|
||||
method,
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
if (method.toLowerCase() !== 'get') {
|
||||
init.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return init;
|
||||
};
|
||||
|
||||
export const handleResp = res => {
|
||||
if (res.status === 401) {
|
||||
throw new Error('Not Authorized to make this request');
|
||||
} else if (res.status > 399) {
|
||||
throw new Error('Error! Status ', res.status);
|
||||
} else if (res.status === 204) {
|
||||
return res.text();
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
email: email => (/^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
|
||||
password: pass => (/^(?=.{8,}).*$/.test(pass)),
|
||||
confirmPassword: () => true,
|
||||
displayName: displayName => (/^(?=.{3,}).*$/.test(displayName))
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import Notification from './notification/Notification';
|
||||
import store from './store/store';
|
||||
import {fetchConfig} from './store/actions/config';
|
||||
import * as itemActions from './store/actions/items';
|
||||
import I18n from './i18n/i18n';
|
||||
import * as notificationActions from './store/actions/notification';
|
||||
import * as authActions from './store/actions/auth';
|
||||
import Notification from './modules/notification/Notification';
|
||||
import store from './store';
|
||||
import {fetchConfig} from './actions/config';
|
||||
import * as itemActions from './actions/items';
|
||||
import I18n from './modules/i18n/i18n';
|
||||
import * as notificationActions from './actions/notification';
|
||||
import * as authActions from './actions/auth';
|
||||
|
||||
export {
|
||||
Notification,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import timeago from 'timeago.js';
|
||||
import esTA from 'timeago.js/locales/es';
|
||||
import esTA from '../../../../node_modules/timeago.js/locales/es';
|
||||
|
||||
/**
|
||||
* Default locales, this should be overriden by config file
|
||||
@@ -0,0 +1,76 @@
|
||||
import {Map} from 'immutable';
|
||||
import * as actions from '../constants/auth';
|
||||
|
||||
const initialState = Map({
|
||||
isLoading: false,
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
showSignInDialog: false,
|
||||
view: 'SIGNIN',
|
||||
error: '',
|
||||
successSignUp: false
|
||||
});
|
||||
|
||||
export default function auth (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SHOW_SIGNIN_DIALOG :
|
||||
return state
|
||||
.set('showSignInDialog', true);
|
||||
case actions.HIDE_SIGNIN_DIALOG :
|
||||
return state.merge(Map({
|
||||
isLoading: false,
|
||||
showSignInDialog: false,
|
||||
view: 'SIGNIN',
|
||||
error: '',
|
||||
successSignUp: false
|
||||
}));
|
||||
case actions.CHANGE_VIEW :
|
||||
return state
|
||||
.set('error', '')
|
||||
.set('view', action.view);
|
||||
case actions.CLEAN_STATE:
|
||||
return initialState;
|
||||
case actions.FETCH_SIGNIN_REQUEST:
|
||||
return state
|
||||
.set('isLoading', true);
|
||||
case actions.FETCH_SIGNIN_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', true)
|
||||
.set('user', action.user);
|
||||
case actions.FETCH_SIGNIN_FAILURE:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('error', action.error);
|
||||
case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
|
||||
return state
|
||||
.set('user', action.user)
|
||||
.set('loggedIn', true);
|
||||
case actions.FETCH_SIGNIN_FACEBOOK_FAILURE:
|
||||
return state
|
||||
.set('error', action.error)
|
||||
.set('user', null);
|
||||
case actions.FETCH_SIGNUP_REQUEST:
|
||||
return state
|
||||
.set('isLoading', true);
|
||||
case actions.FETCH_SIGNUP_FAILURE:
|
||||
return state
|
||||
.set('error', action.error)
|
||||
.set('isLoading', false);
|
||||
case actions.FETCH_SIGNUP_SUCCESS:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('successSignUp', true);
|
||||
case actions.LOGOUT_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', false)
|
||||
.set('user', null);
|
||||
case actions.INVALID_FORM:
|
||||
return state
|
||||
.set('error', action.error);
|
||||
case actions.VALID_FORM:
|
||||
return state
|
||||
.set('error', '');
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import {createStore, applyMiddleware} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from './reducers';
|
||||
@@ -1,17 +0,0 @@
|
||||
/* Auth Actions */
|
||||
|
||||
export const SET_LOGGED_IN_USER = 'SET_LOGGED_IN_USER';
|
||||
export const LOG_OUT_USER = 'LOG_OUT_USER';
|
||||
|
||||
export const setLoggedInUser = (user_id) => {
|
||||
return {
|
||||
type: SET_LOGGED_IN_USER,
|
||||
user_id
|
||||
};
|
||||
};
|
||||
|
||||
export const LogOutUser = () => {
|
||||
return {
|
||||
type: LOG_OUT_USER
|
||||
};
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
/* Auth */
|
||||
|
||||
import * as actions from '../actions/auth';
|
||||
import {fromJS} from 'immutable';
|
||||
|
||||
const initialState = fromJS({});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case actions.SET_LOGGED_IN_USER:
|
||||
return state.set('user', action.user_id);
|
||||
case actions.LOG_OUT_USER:
|
||||
return initialState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"en": {
|
||||
"error": {
|
||||
"email": "Not a valid E-Mail",
|
||||
"password": "Password must be at least 8 characters",
|
||||
"displayName": "Display name is too short",
|
||||
"confirmPassword": "Passwords don`t match. Please, check again",
|
||||
"emailPasswordError": "Email and/or password combination incorrect.",
|
||||
"emailInUse": "Email address already in use"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"error": {
|
||||
"email": "No es un email válido",
|
||||
"password": "La contraseña debe tener por lo menos 8 caracteres",
|
||||
"displayName": "El nombre es muy corto",
|
||||
"confirmPassword": "Las contraseñas no coinciden",
|
||||
"emailPasswordError": "Email y/o contraseña incorrecta.",
|
||||
"emailInUse": "Email address already in use"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@ const name = 'coral-plugin-comment-count';
|
||||
|
||||
const CommentCount = ({items, id}) => {
|
||||
let count = 0;
|
||||
if (items[id]) {
|
||||
count += items[id].comments.length;
|
||||
if (items.assets[id] && items.assets[id].comments) {
|
||||
count += items.assets[id].comments.length;
|
||||
}
|
||||
const itemKeys = Object.keys(items);
|
||||
const itemKeys = Object.keys(items.comments);
|
||||
for (let i = 0; i < itemKeys.length; i++) {
|
||||
const item = items[itemKeys[i]];
|
||||
const item = items.comments[itemKeys[i]];
|
||||
if (item.children) {
|
||||
count += item.children.length;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ class CommentBox extends Component {
|
||||
updateItem: PropTypes.func,
|
||||
id: PropTypes.string,
|
||||
comments: PropTypes.array,
|
||||
reply: PropTypes.bool
|
||||
reply: PropTypes.bool,
|
||||
canPost: PropTypes.bool
|
||||
}
|
||||
|
||||
state = {
|
||||
@@ -51,18 +52,9 @@ class CommentBox extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {styles, reply} = this.props;
|
||||
const {styles, reply, canPost} = this.props;
|
||||
// How to handle language in plugins? Should we have a dependency on our central translation file?
|
||||
return <div>
|
||||
<div className={`${name}-container`}>
|
||||
<input type='text'
|
||||
className={`${name}-username`}
|
||||
style={styles && styles.textarea}
|
||||
value={this.state.username}
|
||||
id={reply ? 'replyUser' : 'commentUser'}
|
||||
placeholder={lang.t('name')}
|
||||
onChange={(e) => this.setState({username: e.target.value})}/>
|
||||
</div>
|
||||
<div
|
||||
className={`${name}-container`}>
|
||||
<label
|
||||
@@ -81,12 +73,15 @@ class CommentBox extends Component {
|
||||
rows={3}/>
|
||||
</div>
|
||||
<div className={`${name}-button-container`}>
|
||||
<button
|
||||
className={`${name}-button`}
|
||||
style={styles && styles.button}
|
||||
onClick={this.postComment}>
|
||||
{lang.t('post')}
|
||||
</button>
|
||||
{ canPost && (
|
||||
<button
|
||||
className={`${name}-button`}
|
||||
style={styles && styles.button}
|
||||
onClick={this.postComment}>
|
||||
{lang.t('post')}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"comment": "Comment",
|
||||
"name": "Name",
|
||||
"comment-post-notif": "Your comment has been posted.",
|
||||
"comment-post-notif-premod": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly."
|
||||
"comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly."
|
||||
},
|
||||
"es": {
|
||||
"post": "Publicar",
|
||||
|
||||
@@ -4,27 +4,35 @@ import translations from './translations.json';
|
||||
|
||||
const name = 'coral-plugin-flags';
|
||||
|
||||
const FlagButton = ({flag, id, postAction, addItem, updateItem, addNotification}) => {
|
||||
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification}) => {
|
||||
const flagged = flag && flag.current_user;
|
||||
const onFlagClick = () => {
|
||||
postAction(id, 'flag', '123', 'comments')
|
||||
.then((action) => {
|
||||
addItem({...action, current_user:true}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, action.id, 'comments');
|
||||
});
|
||||
addNotification('success', lang.t('flag-notif'));
|
||||
if (!flagged) {
|
||||
postAction(id, 'flag', '123', 'comments')
|
||||
.then((action) => {
|
||||
addItem({...action, current_user:true}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, action.id, 'comments');
|
||||
});
|
||||
addNotification('success', lang.t('flag-notif'));
|
||||
} else {
|
||||
deleteAction(id, 'flag', '123', 'comments')
|
||||
.then(() => {
|
||||
updateItem(id, 'flag', '', 'comments');
|
||||
});
|
||||
addNotification('success', lang.t('flag-notif-remove'));
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={`${name }-container`}>
|
||||
<button onClick={onFlagClick} className={`${name }-button`}>
|
||||
<i className={`${name }-icon material-icons`}
|
||||
style={flagged ? styles.flaggedIcon : styles.unflaggedIcon}
|
||||
aria-hidden={true}>flag</i>
|
||||
return <div className={`${name}-container`}>
|
||||
<button onClick={onFlagClick} className={`${name}-button`}>
|
||||
{
|
||||
flagged
|
||||
? <span className={`${name}-button-text`}>{lang.t('flag')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('flagged')}</span>
|
||||
? <span className={`${name}-button-text`}>{lang.t('flagged')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('flag')}</span>
|
||||
}
|
||||
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
|
||||
style={flagged && styles.flaggedIcon}
|
||||
aria-hidden={true}>flag</i>
|
||||
</button>
|
||||
</div>;
|
||||
};
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
"en": {
|
||||
"flag": "Flag",
|
||||
"flagged": "Flagged",
|
||||
"flag-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly."
|
||||
"flag-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.",
|
||||
"flag-notif-remove": "Your flag has been removed."
|
||||
},
|
||||
"es": {
|
||||
"flag": "Marcar",
|
||||
"flagged": "Marcado",
|
||||
"flag-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar."
|
||||
"flag-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar.",
|
||||
"flag-notif-remove": "¡traduceme!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
const packagename = 'coral-plugin-infobox';
|
||||
|
||||
const InfoBox = ({enable, content}) =>
|
||||
<div
|
||||
className={`${packagename}-info ${enable ? null : ', hidden'}` }>
|
||||
{content}
|
||||
</div>;
|
||||
|
||||
export default InfoBox;
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {expect} from 'chai';
|
||||
import InfoBox from '../InfoBox';
|
||||
|
||||
describe('InfoBox', () => {
|
||||
let comment;
|
||||
let render;
|
||||
beforeEach(() => {
|
||||
comment = {};
|
||||
const postItem = (item) => {
|
||||
comment.posted = item;
|
||||
return Promise.resolve(4);
|
||||
};
|
||||
render = shallow(<InfoBox
|
||||
postItem={postItem}
|
||||
updateItem={(e) => comment.text = e.target.value}
|
||||
item_id={'1'}
|
||||
comments={['1', '2', '3']}/>);
|
||||
});
|
||||
|
||||
it('should render the InfoBox appropriately', () => {
|
||||
expect(render.contains('<div class="InfoBox"')).to.be.truthy;
|
||||
expect(render.contains('<button class="postCommentButton"')).to.be.truthy;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
|
||||
const name = 'coral-plugin-flags';
|
||||
|
||||
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem}) => {
|
||||
const liked = like && like.current_user;
|
||||
const onLikeClick = () => {
|
||||
if (!liked) {
|
||||
postAction(id, 'like', '123', 'comments')
|
||||
.then((action) => {
|
||||
addItem({id: action.id, current_user:true, count: like ? like.count + 1 : 1}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, action.id, 'comments');
|
||||
});
|
||||
} else {
|
||||
deleteAction(id, 'like', '123', 'comments')
|
||||
.then(() => {
|
||||
updateItem(like.id, 'count', like.count - 1, 'actions');
|
||||
updateItem(like.id, 'current_user', false, 'actions');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
<button onClick={onLikeClick} className={`${name}-button ${liked && 'likedButton'}`}>
|
||||
{
|
||||
liked
|
||||
? <span className={`${name}-button-text`}>{lang.t('liked')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('like')}</span>
|
||||
}
|
||||
<i className={`${name}-icon material-icons`}
|
||||
aria-hidden={true}>thumb_up</i>
|
||||
<span className={`${name}-like-count`}>{like && like.count > 0 && like.count}</span>
|
||||
</button>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default LikeButton;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"en": {
|
||||
"like": "Like",
|
||||
"liked": "Liked"
|
||||
},
|
||||
"es": {
|
||||
"like": "Me Gusta",
|
||||
"liked": "Me Gustó"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from './translations';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
const name = 'coral-plugin-permalinks';
|
||||
|
||||
@@ -7,9 +7,9 @@ const name = 'coral-plugin-replies';
|
||||
const ReplyButton = (props) => <button
|
||||
className={`${name}-reply-button`}
|
||||
onClick={() => props.updateItem(props.id || props.parent_id, 'showReply', true, 'comments')}>
|
||||
{lang.t('reply')}
|
||||
<i className={`${name}-icon material-icons`}
|
||||
aria-hidden={true}>reply</i>
|
||||
{lang.t('reply')}
|
||||
</button>;
|
||||
|
||||
export default ReplyButton;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
|
||||
const Alert = ({cStyle = 'error', children, className, ...props}) => (
|
||||
<div
|
||||
className={`${styles.alert} ${styles[`alert--${cStyle}`]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Alert;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const ForgotContent = ({changeView, ...props}) => (
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h1>{lang.t('signIn.recoverPassword')}</h1>
|
||||
</div>
|
||||
<form onSubmit={(e) => {e.preventDefault(); props.fetchForgotPassword();}}>
|
||||
<div className={styles.formField}>
|
||||
<label htmlFor="email">{lang.t('signIn.email')}</label>
|
||||
<input type="text" id="email" />
|
||||
</div>
|
||||
<Button type="submit" cStyle="black" className={styles.signInButton}>
|
||||
{lang.t('signIn.recoverPassword')}
|
||||
</Button>
|
||||
</form>
|
||||
<div className={styles.footer}>
|
||||
<span>{lang.t('signIn.needAnAccount')} <a onClick={() => changeView('SIGNUP')}>{lang.t('signIn.register')}</a></span>
|
||||
<span>{lang.t('signIn.alreadyHaveAnAccount')} <a onClick={() => changeView('SIGNIN')}>{lang.t('signIn.signIn')}</a></span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ForgotContent;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
|
||||
const FormField = ({className, showErrors = false, errorMsg, label, ...props}) => (
|
||||
<div className={`${styles.formField} ${className ? className : ''}`}>
|
||||
<label htmlFor={props.id}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
className={showErrors && errorMsg ? styles.error : ''}
|
||||
name={props.id}
|
||||
{...props}
|
||||
/>
|
||||
{showErrors && errorMsg && <span className={styles.errorMsg}><span className={styles.attention}>!</span>{errorMsg}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default FormField;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
|
||||
import SignInContent from './SignInContent';
|
||||
import SingUpContent from './SignUpContent';
|
||||
import ForgotContent from './ForgotContent';
|
||||
|
||||
const SignDialog = ({open, view, handleClose, ...props}) => (
|
||||
<Dialog className={styles.dialog} open={open}>
|
||||
<span className={styles.close} onClick={handleClose}>×</span>
|
||||
{view === 'SIGNIN' && <SignInContent {...props} />}
|
||||
{view === 'SIGNUP' && <SingUpContent {...props} />}
|
||||
{view === 'FORGOT' && <ForgotContent {...props} />}
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
export default SignDialog;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import FormField from './FormField';
|
||||
import Alert from './Alert';
|
||||
import Spinner from 'coral-ui/components/Spinner';
|
||||
import styles from './styles.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const SignInContent = ({handleChange, formData, ...props}) => (
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h1>
|
||||
{lang.t('signIn.signIn')}
|
||||
</h1>
|
||||
</div>
|
||||
<div className={styles.socialConnections}>
|
||||
<Button cStyle="facebook" onClick={props.fetchSignInFacebook}>
|
||||
{lang.t('signIn.facebookSignIn')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h1>
|
||||
{lang.t('signIn.or')}
|
||||
</h1>
|
||||
</div>
|
||||
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
|
||||
<form onSubmit={props.handleSignIn}>
|
||||
<FormField
|
||||
id="email"
|
||||
type="email"
|
||||
label={lang.t('signIn.email')}
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormField
|
||||
id="password"
|
||||
type="password"
|
||||
label={lang.t('signIn.password')}
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div className={styles.action}>
|
||||
{
|
||||
!props.auth.isLoading ?
|
||||
<Button type="submit" cStyle="black" className={styles.signInButton}>
|
||||
{lang.t('signIn.signIn')}
|
||||
</Button>
|
||||
:
|
||||
<Spinner />
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
<div className={styles.footer}>
|
||||
<span><a onClick={() => props.changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')}</a></span>
|
||||
<span>
|
||||
{lang.t('signIn.needAnAccount')}
|
||||
<a onClick={() => props.changeView('SIGNUP')}>
|
||||
{lang.t('signIn.register')}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SignInContent;
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import FormField from './FormField';
|
||||
import Alert from './Alert';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import Spinner from 'coral-ui/components/Spinner';
|
||||
import Success from 'coral-ui/components/Success';
|
||||
import styles from './styles.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const SignUpContent = ({handleChange, formData, ...props}) => (
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h1>
|
||||
{lang.t('signIn.signUp')}
|
||||
</h1>
|
||||
</div>
|
||||
<div className={styles.socialConnections}>
|
||||
<Button cStyle="facebook" onClick={props.fetchSignInFacebook}>
|
||||
{lang.t('signIn.facebookSignUp')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h1>
|
||||
{lang.t('signIn.or')}
|
||||
</h1>
|
||||
</div>
|
||||
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
|
||||
<form onSubmit={props.handleSignUp}>
|
||||
<FormField
|
||||
id="email"
|
||||
type="email"
|
||||
label={lang.t('signIn.email')}
|
||||
value={formData.email}
|
||||
showErrors={props.showErrors}
|
||||
errorMsg={props.errors.email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormField
|
||||
id="displayName"
|
||||
type="text"
|
||||
label={lang.t('signIn.displayName')}
|
||||
value={formData.displayName}
|
||||
showErrors={props.showErrors}
|
||||
errorMsg={props.errors.displayName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormField
|
||||
id="password"
|
||||
type="password"
|
||||
label={lang.t('signIn.password')}
|
||||
value={formData.password}
|
||||
showErrors={props.showErrors}
|
||||
errorMsg={props.errors.password}
|
||||
onChange={handleChange}
|
||||
minLength="8"
|
||||
/>
|
||||
{ !props.errors.password && <span className={styles.hint}> Password must be at least 8 characters. </span> }
|
||||
<FormField
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
label={lang.t('signIn.confirmPassword')}
|
||||
value={formData.confirmPassword}
|
||||
showErrors={props.showErrors}
|
||||
errorMsg={props.errors.confirmPassword}
|
||||
onChange={handleChange}
|
||||
minLength="8"
|
||||
/>
|
||||
<div className={styles.action}>
|
||||
{ !props.auth.isLoading && !props.auth.successSignUp && (
|
||||
<Button type="submit" cStyle="black" className={styles.signInButton}>
|
||||
{lang.t('signIn.signUp')}
|
||||
</Button>
|
||||
)}
|
||||
{ props.auth.isLoading && <Spinner /> }
|
||||
{ !props.auth.isLoading && props.auth.successSignUp && <Success /> }
|
||||
</div>
|
||||
</form>
|
||||
<div className={styles.footer}>
|
||||
<span>
|
||||
{lang.t('signIn.alreadyHaveAnAccount')}
|
||||
<a onClick={() => props.changeView('SIGNIN')}>
|
||||
{lang.t('signIn.signIn')}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SignUpContent;
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
|
||||
const UserBox = ({className, user, logout, ...props}) => (
|
||||
<div
|
||||
className={`${styles.userBox} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
Signed in as <a>{user.displayName}</a>. Not you? <a onClick={logout}>Sign out</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default UserBox;
|
||||
@@ -0,0 +1,131 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 280px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1, .separator h1{
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.formField {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.formField label {
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.formField input {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 10px 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer span {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.socialConnections {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #6b6b6b;
|
||||
}
|
||||
|
||||
input.error{
|
||||
border: solid 2px #f44336;
|
||||
}
|
||||
|
||||
.errorMsg, .hint {
|
||||
color: grey;
|
||||
font-weight: 600;
|
||||
padding: 3px 0 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.userBox a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.attention {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #B71C1C;
|
||||
color: #FFEBEE;
|
||||
font-weight: bolder;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 9px;
|
||||
line-height: 7px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 15px;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import SignDialog from '../components/SignDialog';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
import {
|
||||
changeView,
|
||||
fetchSignUp,
|
||||
fetchSignIn,
|
||||
showSignInDialog,
|
||||
hideSignInDialog,
|
||||
fetchSignInFacebook,
|
||||
fetchForgotPassword,
|
||||
facebookCallback,
|
||||
invalidForm,
|
||||
validForm
|
||||
} from '../../coral-framework/actions/auth';
|
||||
|
||||
class SignInContainer extends Component {
|
||||
initialState = {
|
||||
formData: {
|
||||
email: '',
|
||||
displayName: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
errors: {},
|
||||
showErrors: false
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = this.initialState;
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSignUp = this.handleSignUp.bind(this);
|
||||
this.handleSignIn = this.handleSignIn.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.addError = this.addError.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.authCallback = this.props.facebookCallback;
|
||||
const {formData} = this.state;
|
||||
const errors = Object.keys(formData).reduce((map, prop) => {
|
||||
map[prop] = lang.t('signIn.requiredField');
|
||||
return map;
|
||||
}, {});
|
||||
this.setState({errors});
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
const {name, value} = e.target;
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
formData: {
|
||||
...state.formData,
|
||||
[name]: value
|
||||
}
|
||||
}), () => {
|
||||
this.validation(name, value);
|
||||
});
|
||||
}
|
||||
|
||||
addError(name, error) {
|
||||
return this.setState(state => ({
|
||||
errors: {
|
||||
...state.errors,
|
||||
[name]: error
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
validation(name, value) {
|
||||
const {addError} = this;
|
||||
const {formData} = this.state;
|
||||
|
||||
if (!value.length) {
|
||||
addError(name, lang.t('signIn.requiredField'));
|
||||
} else if (name === 'confirmPassword' && formData.confirmPassword !== formData.password) {
|
||||
addError('confirmPassword', lang.t('signIn.passwordsDontMatch'));
|
||||
} else if (!validate[name](value)) {
|
||||
addError(name, errorMsj[name]);
|
||||
} else {
|
||||
const { [name]: prop, ...errors } = this.state.errors; // eslint-disable-line
|
||||
// Removes Error
|
||||
this.setState(state => ({...state, errors}));
|
||||
}
|
||||
}
|
||||
|
||||
isCompleted() {
|
||||
const {formData} = this.state;
|
||||
return !Object.keys(formData).filter(prop => !formData[prop].length).length;
|
||||
}
|
||||
|
||||
displayErrors(show = true) {
|
||||
this.setState({showErrors: show});
|
||||
}
|
||||
|
||||
handleSignUp(e) {
|
||||
e.preventDefault();
|
||||
const {errors} = this.state;
|
||||
const {fetchSignUp, validForm, invalidForm} = this.props;
|
||||
this.displayErrors();
|
||||
if (this.isCompleted() && !Object.keys(errors).length) {
|
||||
fetchSignUp(this.state.formData);
|
||||
validForm();
|
||||
} else {
|
||||
invalidForm(lang.t('signIn.checkTheForm'));
|
||||
}
|
||||
}
|
||||
|
||||
handleSignIn(e) {
|
||||
e.preventDefault();
|
||||
this.props.fetchSignIn(this.state.formData);
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.props.hideSignInDialog();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {auth, showSignInDialog} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={showSignInDialog}>
|
||||
Sign in to comment
|
||||
</Button>
|
||||
<SignDialog
|
||||
open={auth.showSignInDialog}
|
||||
view={auth.view}
|
||||
{...this}
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
facebookCallback: (err, data) => dispatch(facebookCallback(err, data)),
|
||||
fetchSignUp: formData => dispatch(fetchSignUp(formData)),
|
||||
fetchSignIn: formData => dispatch(fetchSignIn(formData)),
|
||||
fetchSignInFacebook: () => dispatch(fetchSignInFacebook()),
|
||||
fetchForgotPassword: formData => dispatch(fetchForgotPassword(formData)),
|
||||
showSignInDialog: () => dispatch(showSignInDialog()),
|
||||
changeView: view => dispatch(changeView(view)),
|
||||
handleClose: () => dispatch(hideSignInDialog()),
|
||||
invalidForm: error => dispatch(invalidForm(error)),
|
||||
validForm: () => dispatch(validForm())
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(SignInContainer);
|
||||
@@ -0,0 +1,48 @@
|
||||
export default {
|
||||
en: {
|
||||
'signIn': {
|
||||
facebookSignIn: 'Sign in with Facebook',
|
||||
facebookSignUp: 'Sign up with Facebook',
|
||||
logout: 'Logout',
|
||||
signIn: 'Sign In',
|
||||
or: 'Or',
|
||||
email: 'E-mail Address',
|
||||
password: 'Password',
|
||||
forgotYourPass: 'Forgot your password?',
|
||||
needAnAccount: 'Need an account?',
|
||||
register: 'Register',
|
||||
signUp: 'Sign Up',
|
||||
confirmPassword: 'Confirm Password',
|
||||
displayName: 'Display Name',
|
||||
alreadyHaveAnAccount: 'Already have an account?',
|
||||
recoverPassword: 'Recover password',
|
||||
emailInUse: 'Email address already in use',
|
||||
requiredField: 'This field is required',
|
||||
passwordsDontMatch: 'Passwords don\'t match.',
|
||||
checkTheForm: 'Invalid Form. Please, check the fields'
|
||||
}
|
||||
},
|
||||
es: {
|
||||
'signIn': {
|
||||
facebookSignIn: 'Entrar con Facebook',
|
||||
facebookSignUp: 'Regístrate con Facebook',
|
||||
logout: 'Salir',
|
||||
signIn: 'Entrar',
|
||||
or: 'o',
|
||||
email: 'E-mail',
|
||||
password: 'Contraseña',
|
||||
forgotYourPass: 'Has olvidado tu contraseña?',
|
||||
needAnAccount: 'Necesitas una cuenta?',
|
||||
register: 'Regístrate',
|
||||
signUp: 'Registro',
|
||||
confirmPassword: 'Confirmar Contraseña',
|
||||
displayName: 'Nombre',
|
||||
alreadyHaveAnAccount: 'Ya tienes una cuenta?',
|
||||
recoverPassword: 'Recuperar contraseña',
|
||||
emailInUse: 'Este email se encuentra en uso',
|
||||
requiredField: 'Este campo es requerido',
|
||||
passwordsDontMatch: 'Las contraseñas no coinciden',
|
||||
checkTheForm: 'Formulario Inválido. Por favor, completa los campos'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
.button {
|
||||
background: 0 0;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
color: #000;
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 36px;
|
||||
min-width: 64px;
|
||||
padding: 0 8px;
|
||||
display: inline-block;
|
||||
font-family: 'Roboto','Helvetica','Arial',sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
overflow: hidden;
|
||||
will-change: box-shadow,transform;
|
||||
-webkit-transition: box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);
|
||||
transition: box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
line-height: 36px;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.type--black {
|
||||
color: #E0E0E0;
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
.type--local {
|
||||
background: #E0E0E0;
|
||||
color: #212121;
|
||||
}
|
||||
|
||||
.type--facebook {
|
||||
background-color: #4267b2;
|
||||
border-color: #4267b2;
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.type--facebook:hover {
|
||||
background-color: #365899;
|
||||
border-color: #365899;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './Button.css';
|
||||
|
||||
const Button = ({cStyle = 'local', children, className, ...props}) => (
|
||||
<button
|
||||
className={`${styles.button} ${styles[`type--${cStyle}`]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
export default Button;
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import dialogPolyfill from 'dialog-polyfill';
|
||||
import 'dialog-polyfill/dialog-polyfill.css';
|
||||
|
||||
export default class Dialog extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
onCancel: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
open: PropTypes.bool,
|
||||
style: PropTypes.object
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onCancel: e => e.preventDefault(),
|
||||
onClose: e => e.preventDefault()
|
||||
};
|
||||
|
||||
componentDidMount(){
|
||||
const dialog = ReactDOM.findDOMNode(this.refs.dialog);
|
||||
dialogPolyfill.registerDialog(dialog);
|
||||
|
||||
if (this.props.open) {
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
dialog.addEventListener('close', this.props.onClose);
|
||||
dialog.addEventListener('cancel', this.props.onCancel);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const dialog = ReactDOM.findDOMNode(this.refs.dialog);
|
||||
if (this.props.open !== prevProps.open) {
|
||||
if (this.props.open) {
|
||||
dialog.showModal();
|
||||
} else {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const dialog = ReactDOM.findDOMNode(this.refs.dialog);
|
||||
dialog.removeEventListener('cancel', this.props.onCancel);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children, className = '', onClose, onCancel, open, ...rest} = this.props; // eslint-disable-line
|
||||
|
||||
return (
|
||||
<dialog
|
||||
ref="dialog"
|
||||
className={`mdl-dialog ${className}`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
.container {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
-webkit-animation: rotator 1.4s linear infinite;
|
||||
animation: rotator 1.4s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotator {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotator {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
.path {
|
||||
stroke-dasharray: 187;
|
||||
stroke-dashoffset: 0;
|
||||
-webkit-transform-origin: center;
|
||||
transform-origin: center;
|
||||
-webkit-animation: dash 1.4s ease-in-out infinite, colors 5.6s ease-in-out infinite;
|
||||
animation: dash 1.4s ease-in-out infinite, colors 5.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes colors {
|
||||
0% {
|
||||
stroke: #f67150;
|
||||
}
|
||||
100% {
|
||||
stroke: #f6a47e;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes colors {
|
||||
0% {
|
||||
stroke: #f67150;
|
||||
}
|
||||
100% {
|
||||
stroke: #f6a47e;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: 187;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 46.75;
|
||||
-webkit-transform: rotate(135deg);
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 187;
|
||||
-webkit-transform: rotate(450deg);
|
||||
transform: rotate(450deg);
|
||||
}
|
||||
}
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: 187;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 46.75;
|
||||
-webkit-transform: rotate(135deg);
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 187;
|
||||
-webkit-transform: rotate(450deg);
|
||||
transform: rotate(450deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import styles from './Spinner.css';
|
||||
|
||||
const Spinner = () => (
|
||||
<div className={styles.container}>
|
||||
<svg className={styles.spinner} width="40px" height="40px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle className={styles.path} fill="none" strokeWidth="6" strokeLinecap="round" cx="33" cy="33" r="30"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Spinner;
|
||||
@@ -0,0 +1,55 @@
|
||||
.container {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.circle {
|
||||
stroke-dasharray: 166;
|
||||
stroke-dashoffset: 166;
|
||||
stroke-width: 2;
|
||||
stroke-miterlimit: 10;
|
||||
stroke: #00897B;
|
||||
fill: none;
|
||||
animation: stroke .6s cubic-bezier(0.650, 0.000, 0.450, 1.000) forwards;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
stroke-width: 2;
|
||||
stroke: #fff;
|
||||
stroke-miterlimit: 10;
|
||||
margin: 10% auto;
|
||||
box-shadow: inset 0px 0px 0px #00897B;
|
||||
animation: fill .4s ease-in-out .4s forwards, scale .3s ease-in-out .9s both;
|
||||
}
|
||||
|
||||
.check {
|
||||
transform-origin: 50% 50%;
|
||||
stroke-dasharray: 48;
|
||||
stroke-dashoffset: 48;
|
||||
animation: stroke .3s cubic-bezier(0.650, 0.000, 0.450, 1.000) .8s forwards;
|
||||
}
|
||||
|
||||
@keyframes stroke {
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale {
|
||||
0%, 100% {
|
||||
transform: none;
|
||||
}
|
||||
50% {
|
||||
transform: scale3d(1.1, 1.1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fill {
|
||||
100% {
|
||||
box-shadow: inset 0px 0px 0px 30px #00897B;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './Success.css';
|
||||
|
||||
const Success = () => (
|
||||
<div className={styles.container}>
|
||||
<svg className={styles.checkmark} viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle className={styles.circle} cx="26" cy="26" r="25" fill="none"></circle>
|
||||
<path className={styles.check} fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Success;
|
||||
@@ -0,0 +1 @@
|
||||
export {default as Dialog} from './components/Dialog';
|
||||
@@ -10,8 +10,10 @@ services:
|
||||
environment:
|
||||
- "TALK_PORT=5000"
|
||||
- "TALK_MONGO_URL=mongodb://mongo"
|
||||
- "TALK_REDIS_URL=redis://redis"
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
|
||||
mongo:
|
||||
image: mongo:3.2
|
||||
@@ -19,6 +21,14 @@ services:
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
|
||||
redis:
|
||||
image: redis:3.2-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- redis:/data
|
||||
|
||||
volumes:
|
||||
mongo:
|
||||
external: false
|
||||
redis:
|
||||
external: false
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* authorization contains the references to the authorization middleware.
|
||||
* @type {Object}
|
||||
*/
|
||||
const authorization = module.exports = {};
|
||||
|
||||
const debug = require('debug')('talk:middleware:authorization');
|
||||
|
||||
/**
|
||||
* ErrNotAuthorized is an error that is returned in the event an operation is
|
||||
* deemed not authorized.
|
||||
* @type {Error}
|
||||
*/
|
||||
const ErrNotAuthorized = new Error('not authorized');
|
||||
ErrNotAuthorized.status = 401;
|
||||
|
||||
// Add the ErrNotAuthorized error to the authorization object to be exported.
|
||||
authorization.ErrNotAuthorized = ErrNotAuthorized;
|
||||
|
||||
/**
|
||||
* has returns true if the user has all the roles specified, otherwise it will
|
||||
* return false.
|
||||
* @param {Object} user the user to check for roles
|
||||
* @param {Array} roles all the roles that a user must have
|
||||
* @return {Boolean} true if the user has all the roles required, false
|
||||
* otherwise
|
||||
*/
|
||||
authorization.has = (user, ...roles) => roles.every((role) => user.roles.indexOf(role) >= 0);
|
||||
|
||||
/**
|
||||
* needed is a connect middleware layer that ensures that all requests coming
|
||||
* here are both authenticated and match a set of roles required to continue.
|
||||
* @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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
};
|
||||
+11
-1
@@ -62,11 +62,21 @@ AssetSchema.statics.findByUrl = function(url) {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a asset by its url.
|
||||
* @param {String} url identifier of the asset (uuid).
|
||||
*/
|
||||
AssetSchema.statics.findOrCreateByUrl = function(url) {
|
||||
|
||||
return Asset.findOne({url})
|
||||
.then((asset) => asset ? asset
|
||||
: Asset.upsert({url}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Upserts an asset.
|
||||
*/
|
||||
AssetSchema.statics.upsert = function(data) {
|
||||
|
||||
// If an id is not sent, create one.
|
||||
if (typeof data.id === 'undefined') {
|
||||
data.id = uuid.v4();
|
||||
|
||||
+18
-3
@@ -104,14 +104,14 @@ CommentSchema.statics.findByStatusByActionType = function(status, action_type) {
|
||||
return Action
|
||||
.findCommentsIdByActionType(action_type, 'comment')
|
||||
.then((actions) => {
|
||||
|
||||
|
||||
return Comment.find({
|
||||
'status': status,
|
||||
'status': status,
|
||||
'id': {
|
||||
'$in': actions.map(a => {
|
||||
return a.item_id;
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -188,6 +188,21 @@ CommentSchema.statics.removeById = function(id) {
|
||||
return Comment.remove({'id': id});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an action from the comment.
|
||||
* @param {String} id identifier of the comment (uuid)
|
||||
* @param {String} action_type the type of the action to be removed
|
||||
* @param {String} user_id the id of the user performing the action
|
||||
*/
|
||||
CommentSchema.statics.removeAction = function(item_id, user_id, action_type) {
|
||||
return Action.remove({
|
||||
action_type,
|
||||
item_type: 'comment',
|
||||
item_id,
|
||||
user_id
|
||||
});
|
||||
};
|
||||
|
||||
const Comment = mongoose.model('Comment', CommentSchema);
|
||||
|
||||
module.exports = Comment;
|
||||
|
||||
+18
-1
@@ -1,9 +1,18 @@
|
||||
const mongoose = require('../mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
/**
|
||||
* this Schema manages application settings that get used on front- and backend
|
||||
* NOTE: when you set a setting here, it will not automatically be exposed to
|
||||
* the front end. You must add it to the whitelist in the settings route
|
||||
* in /routes/api/settings/index.js
|
||||
* @type {Schema}
|
||||
*/
|
||||
const SettingSchema = new Schema({
|
||||
id: {type: String, default: '1'},
|
||||
moderation: {type: String, enum: ['pre', 'post'], default: 'pre'}
|
||||
moderation: {type: String, enum: ['pre', 'post'], default: 'pre'},
|
||||
infoBoxEnable: {type: Boolean, default: false},
|
||||
infoBoxContent: {type: String, default: ''}
|
||||
}, {
|
||||
timestamps: {
|
||||
createdAt: 'created_at',
|
||||
@@ -35,6 +44,14 @@ SettingSchema.statics.getModerationSetting = function () {
|
||||
return this.findOne({id: '1'}).select('moderation');
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the info box settings and sends it back
|
||||
* @return {Promise} content the content of the info Box
|
||||
*/
|
||||
SettingSchema.statics.getInfoBoxSetting = function () {
|
||||
return this.findOne({id: '1'}).select('infoBoxEnable', 'infoBoxContent');
|
||||
};
|
||||
|
||||
/**
|
||||
* This will update the settings object with whatever you pass in
|
||||
* @param {object} setting a hash of whatever settings you want to update
|
||||
|
||||
+204
-33
@@ -1,31 +1,82 @@
|
||||
const mongoose = require('../mongoose');
|
||||
const uuid = require('uuid');
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run
|
||||
// through during the salting process.
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
// USER_ROLES is the array of roles that is permissible as a user role.
|
||||
const USER_ROLES = [
|
||||
'admin',
|
||||
'moderator'
|
||||
];
|
||||
|
||||
// In the event that the TALK_SESSION_SECRET is missing but we are testing, then
|
||||
// set the process.env.TALK_SESSION_SECRET.
|
||||
if (process.env.NODE_ENV === 'test' && !process.env.TALK_SESSION_SECRET) {
|
||||
process.env.TALK_SESSION_SECRET = 'keyboard cat';
|
||||
} else if (!process.env.TALK_SESSION_SECRET) {
|
||||
throw new Error('TALK_SESSION_SECRET must be defined to encode JSON Web Tokens and other auth functionality');
|
||||
}
|
||||
|
||||
// UserSchema is the mongoose schema defined as the representation of a User in
|
||||
// MongoDB.
|
||||
const UserSchema = new mongoose.Schema({
|
||||
|
||||
// This ID represents the most unique identifier for a user, it is generated
|
||||
// when the user is created as a random uuid.
|
||||
id: {
|
||||
type: String,
|
||||
default: uuid.v4,
|
||||
unique: true,
|
||||
required: true
|
||||
},
|
||||
|
||||
// This is sourced from the social provider or set manually during user setup
|
||||
// and simply provides a name to display for the given user.
|
||||
displayName: String,
|
||||
|
||||
// This is true when the user account is disabled, no action should be
|
||||
// acknowledged when they are disabled. Logins are also prevented.
|
||||
disabled: Boolean,
|
||||
|
||||
// This provides a source of identity proof for users who login using the
|
||||
// local provider. A local provider will be assumed for users who do not
|
||||
// have any social profiles.
|
||||
password: String,
|
||||
profiles: [{
|
||||
|
||||
// Profiles describes the array of identities for a given user. Any one user
|
||||
// can have multiple profiles associated with them, including multiple email
|
||||
// addresses.
|
||||
profiles: [new mongoose.Schema({
|
||||
|
||||
// ID provides the identifier for the user profile, in the case of a local
|
||||
// provider, the id would be an email, in the case of a social provider,
|
||||
// the id would be the foreign providers identifier.
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
// Provider is simply the name attached to the authentication mode. In the
|
||||
// case of a locally provided profile, this will simply be `local`, or a
|
||||
// social provider which for Facebook would just be `facebook`.
|
||||
provider: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}],
|
||||
}, {
|
||||
_id: false
|
||||
})],
|
||||
|
||||
// Roles provides an array of roles (as strings) that is associated with a
|
||||
// user.
|
||||
roles: [String]
|
||||
}, {
|
||||
|
||||
// This will ensure that we have proper timestamps available on this model.
|
||||
timestamps: {
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
@@ -73,6 +124,13 @@ UserSchema.options.toObject.transform = (doc, ret, options) => {
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Create the User model.
|
||||
const UserModel = mongoose.model('User', UserSchema);
|
||||
|
||||
// UserService is the interface for the application to interact with the
|
||||
// UserModel through.
|
||||
const UserService = module.exports = {};
|
||||
|
||||
/**
|
||||
* Finds a user given their email address that we have for them in the system
|
||||
* and ensures that the retuned user matches the password passed in as well.
|
||||
@@ -80,11 +138,15 @@ UserSchema.options.toObject.transform = (doc, ret, options) => {
|
||||
* @param {string} password - password to match against the found user
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
UserSchema.statics.findLocalUser = function(email, password) {
|
||||
return User.findOne({
|
||||
UserService.findLocalUser = (email, password) => {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required for findLocalUser');
|
||||
}
|
||||
|
||||
return UserModel.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: email,
|
||||
id: email.toLowerCase(),
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
@@ -118,13 +180,13 @@ UserSchema.statics.findLocalUser = function(email, password) {
|
||||
* @param {String} srcUserID id of the user to which is the source of the merge
|
||||
* @return {Promise} resolves when the users are merged
|
||||
*/
|
||||
UserSchema.statics.mergeUsers = function(dstUserID, srcUserID) {
|
||||
UserService.mergeUsers = (dstUserID, srcUserID) => {
|
||||
let srcUser, dstUser;
|
||||
|
||||
return Promise
|
||||
.all([
|
||||
User.findOne({id: dstUserID}).exec(),
|
||||
User.findOne({id: srcUserID}).exec()
|
||||
UserModel.findOne({id: dstUserID}).exec(),
|
||||
UserModel.findOne({id: srcUserID}).exec()
|
||||
])
|
||||
.then((users) => {
|
||||
dstUser = users[0];
|
||||
@@ -145,8 +207,8 @@ UserSchema.statics.mergeUsers = function(dstUserID, srcUserID) {
|
||||
* @param {Object} profile - User social/external profile
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
UserSchema.statics.findOrCreateExternalUser = function(profile) {
|
||||
return User
|
||||
UserService.findOrCreateExternalUser = (profile) => {
|
||||
return UserModel
|
||||
.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
@@ -161,7 +223,7 @@ UserSchema.statics.findOrCreateExternalUser = function(profile) {
|
||||
}
|
||||
|
||||
// The user was not found, lets create them!
|
||||
user = new User({
|
||||
user = new UserModel({
|
||||
displayName: profile.displayName,
|
||||
roles: [],
|
||||
profiles: [
|
||||
@@ -176,7 +238,7 @@ UserSchema.statics.findOrCreateExternalUser = function(profile) {
|
||||
});
|
||||
};
|
||||
|
||||
UserSchema.statics.changePassword = function(id, password) {
|
||||
UserService.changePassword = (id, password) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => {
|
||||
if (err) {
|
||||
@@ -187,7 +249,8 @@ UserSchema.statics.changePassword = function(id, password) {
|
||||
});
|
||||
})
|
||||
.then((hashedPassword) => {
|
||||
return User.update({id}, {
|
||||
return UserModel.update({id}, {
|
||||
$inc: {__v: 1},
|
||||
$set: {
|
||||
password: hashedPassword
|
||||
}
|
||||
@@ -200,9 +263,9 @@ UserSchema.statics.changePassword = function(id, password) {
|
||||
* @param {Array} users Users to create
|
||||
* @return {Promise} Resolves with the users that were created
|
||||
*/
|
||||
UserSchema.statics.createLocalUsers = function(users) {
|
||||
UserService.createLocalUsers = (users) => {
|
||||
return Promise.all(users.map((user) => {
|
||||
return User
|
||||
return UserService
|
||||
.createLocalUser(user.email, user.password, user.displayName);
|
||||
}));
|
||||
};
|
||||
@@ -214,11 +277,13 @@ UserSchema.statics.createLocalUsers = function(users) {
|
||||
* @param {String} displayName name of the display user
|
||||
* @param {Function} done callback
|
||||
*/
|
||||
UserSchema.statics.createLocalUser = function(email, password, displayName) {
|
||||
UserService.createLocalUser = (email, password, displayName) => {
|
||||
if (!email) {
|
||||
return Promise.reject('email is required');
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
|
||||
if (!password) {
|
||||
return Promise.reject('password is required');
|
||||
}
|
||||
@@ -233,7 +298,7 @@ UserSchema.statics.createLocalUser = function(email, password, displayName) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
let user = new User({
|
||||
let user = new UserModel({
|
||||
displayName: displayName,
|
||||
password: hashedPassword,
|
||||
roles: [],
|
||||
@@ -247,9 +312,11 @@ UserSchema.statics.createLocalUser = function(email, password, displayName) {
|
||||
|
||||
user.save((err) => {
|
||||
if (err) {
|
||||
if (err.code === 11000) {
|
||||
return reject('Email address already in use');
|
||||
}
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(user);
|
||||
});
|
||||
});
|
||||
@@ -261,8 +328,8 @@ UserSchema.statics.createLocalUser = function(email, password, displayName) {
|
||||
* @param {String} id id of a user
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserSchema.statics.disableUser = function(id) {
|
||||
return User.update({
|
||||
UserService.disableUser = (id) => {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$set: {
|
||||
@@ -276,8 +343,8 @@ UserSchema.statics.disableUser = function(id) {
|
||||
* @param {String} id id of a user
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserSchema.statics.enableUser = function(id) {
|
||||
return User.update({
|
||||
UserService.enableUser = (id) => {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$set: {
|
||||
@@ -292,8 +359,16 @@ UserSchema.statics.enableUser = function(id) {
|
||||
* @param {String} role role to add
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserSchema.statics.addRoleToUser = function(id, role) {
|
||||
return User.update({
|
||||
UserService.addRoleToUser = (id, role) => {
|
||||
|
||||
// Check to see if the user role is in the allowable set of roles.
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
|
||||
// User role is not supported! Error out here.
|
||||
return Promise.reject(new Error(`role ${role} is not supported`));
|
||||
}
|
||||
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$addToSet: {
|
||||
@@ -308,8 +383,8 @@ UserSchema.statics.addRoleToUser = function(id, role) {
|
||||
* @param {String} role role to remove
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserSchema.statics.removeRoleFromUser = function(id, role) {
|
||||
return User.update({
|
||||
UserService.removeRoleFromUser = (id, role) => {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$pull: {
|
||||
@@ -322,21 +397,117 @@ UserSchema.statics.removeRoleFromUser = function(id, role) {
|
||||
* Finds a user with the id.
|
||||
* @param {String} id user id (uuid)
|
||||
*/
|
||||
UserSchema.statics.findById = function(id) {
|
||||
return User.findOne({id});
|
||||
UserService.findById = (id) => {
|
||||
return UserModel.findOne({id});
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds users in an array of idd.
|
||||
* @param {Array} ids array of user identifiers (uuid)
|
||||
*/
|
||||
UserSchema.statics.findByIdArray = function(ids) {
|
||||
return User.find({
|
||||
UserService.findByIdArray = (ids) => {
|
||||
return UserModel.find({
|
||||
'id': {$in: ids}
|
||||
});
|
||||
};
|
||||
|
||||
const User = mongoose.model('User', UserSchema);
|
||||
/**
|
||||
* Creates a JWT from a user email. Only works for local accounts.
|
||||
* @param {String} email of the local user
|
||||
*/
|
||||
UserService.createPasswordResetToken = function (email) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required when creating a JWT for resetting passord');
|
||||
}
|
||||
|
||||
module.exports = User;
|
||||
module.exports.Schema = UserSchema;
|
||||
email = email.toLowerCase();
|
||||
|
||||
return UserModel.findOne({profiles: {$elemMatch: {id: email}}})
|
||||
.then(user => {
|
||||
|
||||
if (user === null) {
|
||||
// since we don't want to reveal that the email does/doesn't exist
|
||||
// just go ahead and resolve the Promise with null and check in the endpoint
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const payload = {email, jti: uuid.v4(), userId: user.id, version: user.__v};
|
||||
const token = jwt.sign(payload, process.env.TALK_SESSION_SECRET, {expiresIn: '1d'});
|
||||
|
||||
return token;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* verifies a jwt and returns the associated user
|
||||
* @param {String} token the JSON Web Token to verify
|
||||
*/
|
||||
UserService.verifyPasswordResetToken = token => {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, process.env.TALK_SESSION_SECRET, (error, decoded) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
resolve(decoded);
|
||||
});
|
||||
})
|
||||
.then(decoded => {
|
||||
/**
|
||||
* TODO: check the jti from this decoded token in redis
|
||||
* and make an entry if it does not exist.
|
||||
* reject if entry already exists.
|
||||
*/
|
||||
return UserService.findById(decoded.userId);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a user using a value which gets compared using a prefix match against
|
||||
* the user's email address and/or their display name.
|
||||
* @param {String} value value to search by
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.search = (value) => {
|
||||
return UserModel.find({
|
||||
$or: [
|
||||
|
||||
// Search by a prefix match on the displayName.
|
||||
{
|
||||
'displayName': {
|
||||
$regex: new RegExp(`^${value}`),
|
||||
$options: 'i'
|
||||
}
|
||||
},
|
||||
|
||||
// Search by a prefix match on the email address.
|
||||
{
|
||||
'profiles': {
|
||||
$elemMatch: {
|
||||
id: {
|
||||
$regex: new RegExp(`^${value}`),
|
||||
$options: 'i'
|
||||
},
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a count of the current users.
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.count = () => {
|
||||
return UserModel.count();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all the users.
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.all = () => {
|
||||
return UserModel.find();
|
||||
};
|
||||
|
||||
+6
-10
@@ -10,16 +10,12 @@ if (enabled('talk:db')) {
|
||||
mongoose.set('debug', true);
|
||||
}
|
||||
|
||||
try {
|
||||
mongoose.connect(url, (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
mongoose.connect(url, (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
debug('Connected to MongoDB!');
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Cannot stablish a connection with MongoDB', err);
|
||||
}
|
||||
debug('connection established');
|
||||
});
|
||||
|
||||
module.exports = mongoose;
|
||||
|
||||
+19
-5
@@ -5,16 +5,15 @@
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "./bin/www",
|
||||
"build": "webpack --config webpack.config.js --bail",
|
||||
"build-watch": "webpack --config webpack.config.dev.js --watch",
|
||||
"build": "NODE_ENV=production webpack --config webpack.config.js --bail",
|
||||
"build-watch": "NODE_ENV=development webpack --config webpack.config.dev.js --watch",
|
||||
"lint": "eslint bin/* .",
|
||||
"lint-fix": "eslint . --fix",
|
||||
"pretest": "npm install",
|
||||
"test": "mocha --compilers js:babel-core/register --recursive tests",
|
||||
"test-watch": "mocha --compilers js:babel-core/register --recursive -w tests",
|
||||
"embed-start": "npm run build && ./bin/www",
|
||||
"pree2e": "./pree2e.sh",
|
||||
"e2e": "node_modules/.bin/nightwatch"
|
||||
"e2e": "node_modules/.bin/nightwatch",
|
||||
"embed-start": "NODE_ENV=development npm run build && ./bin/www"
|
||||
},
|
||||
"config": {
|
||||
"pre-git": {
|
||||
@@ -49,13 +48,26 @@
|
||||
"dependencies": {
|
||||
"bcrypt": "^0.8.7",
|
||||
"body-parser": "^1.15.2",
|
||||
"cli-table": "^0.3.1",
|
||||
"commander": "^2.9.0",
|
||||
"connect-redis": "^3.1.0",
|
||||
"debug": "^2.2.0",
|
||||
"ejs": "^2.5.2",
|
||||
"express": "^4.14.0",
|
||||
"express-session": "^1.14.2",
|
||||
"helmet": "^3.1.0",
|
||||
"jsonwebtoken": "^7.1.9",
|
||||
"lodash": "^4.16.6",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"mongoose": "^4.6.5",
|
||||
"morgan": "^1.7.0",
|
||||
"nodemailer": "^2.6.4",
|
||||
"passport": "^0.3.2",
|
||||
"passport-facebook": "^2.1.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"prompt": "^1.0.0",
|
||||
"react-linkify": "^0.1.3",
|
||||
"redis": "^2.6.3",
|
||||
"uuid": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -78,6 +90,7 @@
|
||||
"chai-http": "^3.0.0",
|
||||
"copy-webpack-plugin": "^4.0.0",
|
||||
"css-loader": "^0.25.0",
|
||||
"dialog-polyfill": "^0.4.4",
|
||||
"eslint": "^3.9.1",
|
||||
"eslint-config-postcss": "^2.0.2",
|
||||
"eslint-config-standard": "^6.2.1",
|
||||
@@ -107,6 +120,7 @@
|
||||
"react": "15.3.2",
|
||||
"react-dom": "15.3.2",
|
||||
"react-mdl": "^1.7.2",
|
||||
"react-mdl-selectfield": "^0.2.0",
|
||||
"react-onclickoutside": "^5.7.1",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^3.0.0",
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
const passport = require('passport');
|
||||
const User = require('./models/user');
|
||||
const LocalStrategy = require('passport-local').Strategy;
|
||||
const FacebookStrategy = require('passport-facebook').Strategy;
|
||||
|
||||
//==============================================================================
|
||||
// SESSION SERIALIZATION
|
||||
//==============================================================================
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id);
|
||||
});
|
||||
|
||||
passport.deserializeUser((id, done) => {
|
||||
User
|
||||
.findById(id)
|
||||
.then((user) => {
|
||||
done(null, user);
|
||||
})
|
||||
.catch((err) => {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Validates that a user is allowed to login.
|
||||
* @param {User} user the user to be validated
|
||||
* @param {Function} done the callback for the validation
|
||||
*/
|
||||
function ValidateUserLogin(user, done) {
|
||||
if (!user) {
|
||||
return done(new Error('user not found'));
|
||||
}
|
||||
|
||||
if (user.disabled) {
|
||||
return done(null, false, {message: 'Account disabled'});
|
||||
}
|
||||
|
||||
return done(null, user);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// STRATEGIES
|
||||
//==============================================================================
|
||||
|
||||
passport.use(new LocalStrategy({
|
||||
usernameField: 'email',
|
||||
passwordField: 'password'
|
||||
}, (email, password, done) => {
|
||||
User
|
||||
.findLocalUser(email, password)
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
return done(null, false, {message: 'Incorrect email/password combination'});
|
||||
}
|
||||
|
||||
return ValidateUserLogin(user, done);
|
||||
})
|
||||
.catch((err) => {
|
||||
done(err);
|
||||
});
|
||||
}));
|
||||
|
||||
if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET && process.env.TALK_ROOT_URL) {
|
||||
passport.use(new FacebookStrategy({
|
||||
clientID: process.env.TALK_FACEBOOK_APP_ID,
|
||||
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
|
||||
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
|
||||
profileFields: ['id', 'displayName', 'picture.type(large)']
|
||||
}, (accessToken, refreshToken, profile, done) => {
|
||||
User
|
||||
.findOrCreateExternalUser(profile)
|
||||
.then((user) =>
|
||||
ValidateUserLogin(user, done)
|
||||
)
|
||||
.catch((err) => {
|
||||
done(err);
|
||||
});
|
||||
}));
|
||||
} else {
|
||||
console.error('Facebook cannot be enabled, missing one of TALK_FACEBOOK_APP_ID, TALK_FACEBOOK_APP_SECRET, TALK_ROOT_URL');
|
||||
}
|
||||
|
||||
module.exports = passport;
|
||||
@@ -0,0 +1,39 @@
|
||||
const redis = require('redis');
|
||||
const debug = require('debug')('talk:redis');
|
||||
const url = process.env.TALK_REDIS_URL || 'redis://localhost';
|
||||
|
||||
const client = redis.createClient(url, {
|
||||
retry_strategy: function(options) {
|
||||
if (options.error && options.error.code === 'ECONNREFUSED') {
|
||||
|
||||
// End reconnecting on a specific error and flush all commands with a individual error
|
||||
return new Error('The server refused the connection');
|
||||
}
|
||||
if (options.total_retry_time > 1000 * 60 * 60) {
|
||||
|
||||
// End reconnecting after a specific timeout and flush all commands with a individual error
|
||||
return new Error('Retry time exhausted');
|
||||
}
|
||||
|
||||
if (options.times_connected > 10) {
|
||||
|
||||
// End reconnecting with built in error
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// reconnect after
|
||||
return Math.max(options.attempt * 100, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
client.ping((err) => {
|
||||
if (err) {
|
||||
console.error('Can\'t ping the redis server!');
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
debug('connection established');
|
||||
});
|
||||
|
||||
module.exports = client;
|
||||
@@ -1,11 +1,15 @@
|
||||
const express = require('express');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/embed/stream/preview', (req, res) => {
|
||||
res.render('embed-stream', {basePath: '/client/embed/stream'});
|
||||
});
|
||||
|
||||
router.get('/password-reset/:token', (req, res, next) => {
|
||||
// render a page or something?
|
||||
res.send('ok');
|
||||
});
|
||||
|
||||
router.get('*', (req, res) => {
|
||||
res.render('admin', {basePath: '/client/coral-admin'});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
const express = require('express');
|
||||
const passport = require('../../../passport');
|
||||
const authorization = require('../../../middleware/authorization');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* This returns the user if they are logged in.
|
||||
*/
|
||||
router.get('/', authorization.needed(), (req, res) => {
|
||||
res.json(req.user);
|
||||
});
|
||||
|
||||
/**
|
||||
* This destroys the session of a user, if they have one.
|
||||
*/
|
||||
router.delete('/', (req, res) => {
|
||||
req.session.destroy(() => {
|
||||
res.status(204).end();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This sends back the user data as JSON.
|
||||
*/
|
||||
const HandleAuthCallback = (req, res, next) => (err, user) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return next(authorization.ErrNotAuthorized);
|
||||
}
|
||||
|
||||
// Perform the login of the user!
|
||||
req.logIn(user, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// We logged in the user! Let's send back the user data.
|
||||
res.json({user});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the response to the login attempt via a popup callback with some JS.
|
||||
*/
|
||||
const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
|
||||
if (err) {
|
||||
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.render('auth-callback', {err: JSON.stringify(authorization.ErrNotAuthorized), data: null});
|
||||
}
|
||||
|
||||
// Perform the login of the user!
|
||||
req.logIn(user, (err) => {
|
||||
if (err) {
|
||||
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
|
||||
}
|
||||
|
||||
// We logged in the user! Let's send back the user data.
|
||||
res.render('auth-callback', {err: null, data: JSON.stringify(user)});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Local auth endpoint, will recieve a email and password
|
||||
*/
|
||||
router.post('/local', (req, res, next) => {
|
||||
|
||||
// Perform the local authentication.
|
||||
passport.authenticate('local', HandleAuthCallback(req, res, next))(req, res, next);
|
||||
});
|
||||
|
||||
/**
|
||||
* Facebook auth endpoint, this will redirect the user immediatly to facebook
|
||||
* for authorization.
|
||||
*/
|
||||
router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
|
||||
|
||||
/**
|
||||
* Facebook callback endpoint, this will send the user a html page designed to
|
||||
* send back the user credentials upon sucesfull login.
|
||||
*/
|
||||
router.get('/facebook/callback', (req, res, next) => {
|
||||
|
||||
// Perform the facebook login flow and pass the data back through the opener.
|
||||
passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -126,7 +126,19 @@ router.delete('/:comment_id', (req, res, next) => {
|
||||
Comment
|
||||
.removeById(req.params.comment_id)
|
||||
.then(() => {
|
||||
res.status(201).send('OK. Removed');
|
||||
res.status(201).send({});
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
});
|
||||
});
|
||||
|
||||
router.delete('/:comment_id/actions', (req, res, next) => {
|
||||
console.log(req.params);
|
||||
Comment
|
||||
.removeAction(req.params.comment_id, req.body.user_id, req.body.action_type)
|
||||
.then(() => {
|
||||
res.status(201).send({});
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
|
||||
@@ -3,6 +3,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/asset', require('./asset'));
|
||||
router.use('/auth', require('./auth'));
|
||||
router.use('/comments', require('./comments'));
|
||||
router.use('/queue', require('./queue'));
|
||||
router.use('/settings', require('./settings'));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const _ = require('lodash');
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Setting = require('../../../models/setting');
|
||||
@@ -5,7 +6,10 @@ const Setting = require('../../../models/setting');
|
||||
router.get('/', (req, res, next) => {
|
||||
Setting
|
||||
.getSettings()
|
||||
.then(settings => res.json(settings))
|
||||
.then(settings => {
|
||||
const whitelist = ['moderation'];
|
||||
res.json(_.pick(settings, whitelist));
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,35 +3,45 @@ const express = require('express');
|
||||
const Comment = require('../../../models/comment');
|
||||
const User = require('../../../models/user');
|
||||
const Action = require('../../../models/action');
|
||||
const Asset = require('../../../models/asset');
|
||||
|
||||
const Setting = require('../../../models/setting');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Find all the comments by a specific asset_id.
|
||||
// Find all the comments by a specific asset_url.
|
||||
// . if pre: get the comments that are accepted.
|
||||
// . if post: get the comments that are new and accepted.
|
||||
router.get('/', (req, res, next) => {
|
||||
const commentsPromise = Setting.getModerationSetting().then(({moderation}) => {
|
||||
|
||||
// Get the asset_id for this url (or create it if it doesn't exist)
|
||||
Promise.all([
|
||||
Asset.findOrCreateByUrl(decodeURIComponent(req.query.asset_url)),
|
||||
Setting.getModerationSetting()
|
||||
])
|
||||
.then(([asset, {moderation}]) => {
|
||||
// Get the sitewide moderation setting and return the appropriate comments
|
||||
switch(moderation){
|
||||
case 'pre':
|
||||
return Comment.findAcceptedByAssetId(req.query.asset_id);
|
||||
return Promise.all([Comment.findAcceptedByAssetId(asset.id), asset]);
|
||||
case 'post':
|
||||
return Comment.findAcceptedAndNewByAssetId(req.query.asset_id);
|
||||
return Promise.all([Comment.findAcceptedAndNewByAssetId(asset.id), asset]);
|
||||
default:
|
||||
throw new Error('Moderation setting not found.');
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
// Get all the users and actions for those comments.
|
||||
commentsPromise.then(comments => {
|
||||
.then(([comments, asset]) => {
|
||||
return Promise.all([
|
||||
[asset],
|
||||
comments,
|
||||
User.findByIdArray(comments.map((comment) => comment.author_id)),
|
||||
Action.getActionSummaries(comments.map((comment) => comment.id))
|
||||
]);
|
||||
}).then(([comments, users, actions]) => {
|
||||
})
|
||||
.then(([assets, comments, users, actions]) => {
|
||||
res.json({
|
||||
assets,
|
||||
comments,
|
||||
users,
|
||||
actions
|
||||
|
||||
+97
-23
@@ -1,6 +1,12 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const User = require('../../../models/user');
|
||||
const mailer = require('../../../services/mailer');
|
||||
const ejs = require('ejs');
|
||||
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());
|
||||
|
||||
router.get('/', (req, res, next) => {
|
||||
const {
|
||||
@@ -11,28 +17,9 @@ router.get('/', (req, res, next) => {
|
||||
limit = 50 // Total Per Page
|
||||
} = req.query;
|
||||
|
||||
let q = {
|
||||
$or: [
|
||||
{
|
||||
'displayName': {
|
||||
$regex: new RegExp(`^${value}`),
|
||||
$options: 'i'
|
||||
},
|
||||
'profiles': {
|
||||
$elemMatch: {
|
||||
id: {
|
||||
$regex: new RegExp(`^${value}`),
|
||||
$options: 'i'
|
||||
},
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
Promise.all([
|
||||
User.find(q)
|
||||
User
|
||||
.search(value)
|
||||
.sort({[field]: (asc === 'true') ? 1 : -1})
|
||||
.skip((page - 1) * limit)
|
||||
.limit(limit),
|
||||
@@ -40,11 +27,13 @@ router.get('/', (req, res, next) => {
|
||||
])
|
||||
.then(([data, count]) => {
|
||||
const users = data.map((user) => {
|
||||
const {displayName, created_at} = user;
|
||||
const {id, displayName, created_at} = user;
|
||||
return {
|
||||
id,
|
||||
displayName,
|
||||
created_at,
|
||||
profiles: user.toObject().profiles
|
||||
profiles: user.toObject().profiles,
|
||||
roles: user.toObject().roles
|
||||
};
|
||||
});
|
||||
|
||||
@@ -60,4 +49,89 @@ router.get('/', (req, res, next) => {
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
router.post('/:user_id/role', (req, res, next) => {
|
||||
User
|
||||
.addRoleToUser(req.params.user_id, req.body.role)
|
||||
.then(role => {
|
||||
res.send(role);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
router.post('/', (req, res, next) => {
|
||||
const {email, password, displayName} = req.body;
|
||||
|
||||
User
|
||||
.createLocalUser(email, password, displayName)
|
||||
.then(user => {
|
||||
res.status(201).send(user);
|
||||
})
|
||||
.catch(err => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* expects 2 fields in the body of the request
|
||||
* 1) the token that was in the url of the email link {String}
|
||||
* 2) the new password {String}
|
||||
*/
|
||||
router.post('/update-password', (req, res, next) => {
|
||||
const {token, password} = req.body;
|
||||
|
||||
User.verifyPasswordResetToken(token)
|
||||
.then(user => {
|
||||
return User.changePassword(user.id, password);
|
||||
})
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
res.status(401).send('Not Authorized');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* this endpoint takes an email (username) and checks if it belongs to a User account
|
||||
* if it does, create a JWT and send an email
|
||||
*/
|
||||
router.post('/request-password-reset', (req, res, next) => {
|
||||
const {email} = req.body;
|
||||
|
||||
if (!email) {
|
||||
return next();
|
||||
}
|
||||
|
||||
User
|
||||
.createPasswordResetToken(email)
|
||||
.then(token => {
|
||||
if (token === null) {
|
||||
return Promise.resolve('the email was not found in the db.');
|
||||
}
|
||||
|
||||
const options = {
|
||||
subject: 'Password Reset Requested - Talk',
|
||||
from: 'noreply@coralproject.net',
|
||||
to: email,
|
||||
html: resetEmailTemplate({
|
||||
token,
|
||||
// probably more clear to explicitly pass this
|
||||
rootURL: process.env.TALK_ROOT_URL
|
||||
})
|
||||
};
|
||||
return mailer.sendSimple(options);
|
||||
})
|
||||
.then(() => {
|
||||
// we want to send a 204 regardless of the user being found in the db
|
||||
// if we fail on missing emails, it would reveal if people are registered or not.
|
||||
res.status(204).send('OK');
|
||||
})
|
||||
.catch(error => {
|
||||
const errorMsg = typeof error === 'string' ? error : error.message;
|
||||
|
||||
res.status(500).json({error: errorMsg});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
+5
-1
@@ -6,7 +6,11 @@ router.use('/admin', require('./admin'));
|
||||
router.use('/embed', require('./embed'));
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
return res.render('home', {});
|
||||
return res.render('article', {title: 'Coral Talk'});
|
||||
});
|
||||
|
||||
router.get('/assets/:asset_title', (req, res) => {
|
||||
return res.render('article', {title: req.params.asset_title.split('-').join(' ')});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
if (!process.env.TALK_SMTP_CONNECTION_URL) {
|
||||
console.error('TALK_SMTP_CONNECTION_URL should be defined if you would like to send password reset emails from Talk');
|
||||
}
|
||||
|
||||
const defaultTransporter = nodemailer.createTransport(process.env.TALK_SMTP_CONNECTION_URL);
|
||||
|
||||
const mailer = {
|
||||
|
||||
/**
|
||||
* sendSimple
|
||||
*
|
||||
* @param {Object} {from, to, subject, text = '', html = ''}
|
||||
* @returns
|
||||
*/
|
||||
sendSimple({from, to, subject, text = '', html = '', transporter = defaultTransporter}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!from) {
|
||||
reject('sendSimple requires a from address');
|
||||
}
|
||||
if (!to) {
|
||||
reject('sendSimple requires a comma-separated list of "to" addresses');
|
||||
}
|
||||
if (!subject) {
|
||||
reject('sendSimple requires a subject for the email');
|
||||
}
|
||||
|
||||
return resolve(transporter.sendMail({from, to, subject, text, html}));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = mailer;
|
||||
+31
-1
@@ -162,6 +162,36 @@ paths:
|
||||
responses:
|
||||
204:
|
||||
description: OK
|
||||
|
||||
/user/request-password-reset:
|
||||
post:
|
||||
tags:
|
||||
- Users
|
||||
description: trigger a reset password email. sends a success code whether email was found or no.
|
||||
responses:
|
||||
204:
|
||||
description: OK
|
||||
|
||||
/user/update-password:
|
||||
post:
|
||||
tags:
|
||||
- Users
|
||||
description: Update existing user password
|
||||
parameters:
|
||||
- name: token
|
||||
type: string
|
||||
in: body
|
||||
description: JSON Web token taken taken from emailed link
|
||||
required: true
|
||||
- name: password
|
||||
type: string
|
||||
in: body
|
||||
description: new password to be settings
|
||||
required: true
|
||||
responses:
|
||||
204:
|
||||
description: OK
|
||||
|
||||
/asset:
|
||||
get:
|
||||
tags:
|
||||
@@ -276,7 +306,7 @@ definitions:
|
||||
description: A summary of the asset, inferred on initial load.
|
||||
section:
|
||||
type: string
|
||||
description: The section the asset is in.
|
||||
description: The section the asset is in.
|
||||
subsection:
|
||||
type: string
|
||||
description: The subsection that the asset is in.
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import {Map} from 'immutable';
|
||||
import {expect} from 'chai';
|
||||
import authReducer from '../../../../client/coral-framework/store/reducers/auth';
|
||||
import * as actions from '../../../../client/coral-framework/store/actions/auth';
|
||||
|
||||
describe ('authReducer', () => {
|
||||
describe('SET_LOGGED_IN_USER', () => {
|
||||
it('should set a logged in user', () => {
|
||||
const action = {
|
||||
type: actions.SET_LOGGED_IN_USER,
|
||||
user_id: '123'
|
||||
};
|
||||
const store = new Map({});
|
||||
const result = authReducer(store, action);
|
||||
expect(result.get('user')).to.equal(action.user_id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LOG_OUT_USER', () => {
|
||||
it('should clear the user store', () => {
|
||||
const action = {
|
||||
type: actions.LOG_OUT_USER
|
||||
};
|
||||
const store = new Map({
|
||||
user: '123'
|
||||
});
|
||||
const result = authReducer(store, action);
|
||||
expect(result.get('user')).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import 'react';
|
||||
import 'redux';
|
||||
import {expect} from 'chai';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import * as actions from '../../../../client/coral-framework/store/actions/items';
|
||||
import * as actions from '../../../../client/coral-framework/actions/items';
|
||||
import {Map} from 'immutable';
|
||||
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -18,8 +18,11 @@ describe('itemActions', () => {
|
||||
});
|
||||
|
||||
describe('getStream', () => {
|
||||
const rootId = '1234';
|
||||
const assetUrl = 'http://www.test.com';
|
||||
const response = {
|
||||
assets: [{
|
||||
id: '1234', url: assetUrl
|
||||
}],
|
||||
comments: [
|
||||
{body: 'stuff', id: '123'},
|
||||
{body: 'morestuff', id: '456'}
|
||||
@@ -42,17 +45,17 @@ describe('itemActions', () => {
|
||||
|
||||
it('should get an stream from an asset_id and send the appropriate dispatches', () => {
|
||||
fetchMock.get('*', JSON.stringify(response));
|
||||
return actions.getStream(rootId)(store.dispatch)
|
||||
return actions.getStream(assetUrl)(store.dispatch)
|
||||
.then((res) => {
|
||||
expect(fetchMock.calls().matched[0][0]).to.equal('/api/v1/stream?asset_id=1234');
|
||||
expect(fetchMock.calls().matched[0][0]).to.equal('/api/v1/stream?asset_url=http%3A%2F%2Fwww.test.com');
|
||||
expect(res).to.deep.equal(response);
|
||||
expect(store.getActions()[0]).to.deep.equal({
|
||||
expect(store.getActions()[1]).to.deep.equal({
|
||||
type: actions.ADD_ITEM,
|
||||
item: response.comments[0],
|
||||
item_type: 'comments',
|
||||
id: '123'
|
||||
});
|
||||
expect(store.getActions()[1]).to.deep.equal({
|
||||
expect(store.getActions()[2]).to.deep.equal({
|
||||
type: actions.ADD_ITEM,
|
||||
item: response.comments[1],
|
||||
item_type: 'comments',
|
||||
@@ -62,7 +65,7 @@ describe('itemActions', () => {
|
||||
});
|
||||
it('should handle an error', () => {
|
||||
fetchMock.get('*', 404);
|
||||
return actions.getStream(rootId)(store.dispatch)
|
||||
return actions.getStream(assetUrl)(store.dispatch)
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy;
|
||||
});
|
||||
@@ -119,6 +122,7 @@ describe('itemActions', () => {
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type':'application/json'
|
||||
},
|
||||
body: JSON.stringify(item.data)
|
||||
@@ -162,6 +166,24 @@ describe('itemActions', () => {
|
||||
expect(err).to.be.truthy;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAction', () => {
|
||||
it ('should remove an action', () => {
|
||||
fetchMock.delete('*', {});
|
||||
return actions.deleteAction('abc', 'flag', '123', 'comments')(store.dispatch)
|
||||
.then(response => {
|
||||
expect(fetchMock.calls().matched[0][0]).to.equal('/api/v1/comments/abc/actions');
|
||||
expect(response).to.deep.equal({});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle an error', () => {
|
||||
fetchMock.post('*', 404);
|
||||
return actions.postAction('abc', 'flag', '123')(store.dispatch)
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Map, fromJS} from 'immutable';
|
||||
import {expect} from 'chai';
|
||||
import itemsReducer from '../../../../client/coral-framework/store/reducers/items';
|
||||
import itemsReducer from '../../../../client/coral-framework/reducers/items';
|
||||
|
||||
describe ('itemsReducer', () => {
|
||||
describe('ADD_ITEM', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Map} from 'immutable';
|
||||
import {expect} from 'chai';
|
||||
import notificationReducer from '../../../../client/coral-framework/store/reducers/notification';
|
||||
import * as actions from '../../../../client/coral-framework/store/actions/notification';
|
||||
import notificationReducer from '../../../../client/coral-framework/reducers/notification';
|
||||
import * as actions from '../../../../client/coral-framework/actions/notification';
|
||||
|
||||
describe ('notificationsReducer', () => {
|
||||
describe('ADD_NOTIFICATION', () => {
|
||||
|
||||
+97
-84
@@ -1,95 +1,108 @@
|
||||
/* eslint-env node, mocha */
|
||||
|
||||
require('../utils/mongoose');
|
||||
|
||||
const chai = require('chai');
|
||||
const expect = chai.expect;
|
||||
const server = require('../../app');
|
||||
const Asset = require('../../models/asset');
|
||||
const expect = require('chai').expect;
|
||||
|
||||
// Setup chai.
|
||||
chai.should();
|
||||
chai.use(require('chai-http'));
|
||||
describe('Asset: model', () => {
|
||||
|
||||
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']
|
||||
};
|
||||
beforeEach(() => {
|
||||
const defaults = {url:'http://test.com'};
|
||||
return Asset.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
|
||||
});
|
||||
|
||||
describe('Asset: models', () => {
|
||||
|
||||
describe('/GET Asset', () => {
|
||||
describe('#get', () => {
|
||||
it('It should get an empty array when there are no assets.', (done) => {
|
||||
|
||||
chai.request(server)
|
||||
.get('/api/v1/asset')
|
||||
.end((err, res) => {
|
||||
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
|
||||
res.should.have.status(200);
|
||||
res.body.should.be.a('array');
|
||||
res.body.length.should.be.eql(0);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
describe('#findById', ()=> {
|
||||
it('should find an asset by the id', () => {
|
||||
return Asset.findById(1)
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://test.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 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();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#findByUrl', ()=> {
|
||||
it('should find an asset by a url', () => {
|
||||
return Asset.findByUrl('http://test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://test.com');
|
||||
});
|
||||
});
|
||||
}); // End describe /PUT Asset
|
||||
|
||||
it('should return null when a url does not exist', () => {
|
||||
return Asset.findByUrl('http://new.test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.be.null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findOrCreateByUrl', ()=> {
|
||||
it('should find an asset by a url', () => {
|
||||
return Asset.findOrCreateByUrl('http://test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://test.com');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a new asset when the url does not exist', () => {
|
||||
return Asset.findOrCreateByUrl('http://new.test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('id')
|
||||
.and.to.not.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findOrCreateByUrl', ()=> {
|
||||
it('should find an asset by a url', () => {
|
||||
return Asset.findOrCreateByUrl('http://test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://test.com');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a new asset when the url does not exist', () => {
|
||||
return Asset.findOrCreateByUrl('http://new.test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('id')
|
||||
.and.to.not.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upsert', ()=> {
|
||||
it('should insert an asset with no id', () => {
|
||||
return Asset.upsert({url: 'http://newasset.test.com'})
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('id');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update an asset when the id exists', () => {
|
||||
return Asset.upsert({id: 1, url: 'http://new.test.com'})
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('id')
|
||||
.and.to.equal('1');
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://new.test.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#removeAll', ()=> {
|
||||
it('should insert an asset with no id', () => {
|
||||
return Asset.removeAll({id:1})
|
||||
.then(() => {
|
||||
return Asset.findById(1);
|
||||
})
|
||||
.then((result) => {
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,4 +134,15 @@ describe('Comment: models', () => {
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
describe('#removeAction', () => {
|
||||
it('should remove an action', () => {
|
||||
return Comment.removeAction('3', '123', 'flag').then(() => {
|
||||
return Action.findByItemIdArray(['123']);
|
||||
})
|
||||
.then((actions) => {
|
||||
expect(actions.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+10
-2
@@ -8,7 +8,7 @@ const expect = require('chai').expect;
|
||||
describe('Setting: model', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const defaults = {id: 1, moderation: 'pre'};
|
||||
const defaults = {id: 1};
|
||||
return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
|
||||
});
|
||||
|
||||
@@ -18,13 +18,21 @@ describe('Setting: model', () => {
|
||||
expect(settings).to.have.property('moderation').and.to.equal('pre');
|
||||
});
|
||||
});
|
||||
it('should have two infoBox fields defined', () => {
|
||||
return Setting.getSettings().then(settings => {
|
||||
expect(settings).to.have.property('infoBoxEnable').and.to.equal(false);
|
||||
expect(settings).to.have.property('infoBoxContent').and.to.equal('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateSettings()', () => {
|
||||
it('should update the settings with a passed object', () => {
|
||||
const mockSettings = {moderation: 'post'};
|
||||
const mockSettings = {moderation: 'post', infoBoxEnable: true, infoBoxContent: 'yeah'};
|
||||
return Setting.updateSettings(mockSettings).then(updatedSettings => {
|
||||
expect(updatedSettings).to.have.property('moderation').and.to.equal('post');
|
||||
expect(updatedSettings).to.have.property('infoBoxEnable', true);
|
||||
expect(updatedSettings).to.have.property('infoBoxContent', 'yeah');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user