mirror of
https://github.com/wassname/talk.git
synced 2026-06-29 08:32:37 +08:00
Merge branch 'master' of https://github.com/coralproject/talk into stream-e2e
This commit is contained in:
@@ -8,3 +8,4 @@ tests/e2e/reports
|
||||
*.iml
|
||||
.env*
|
||||
gaba.cfg
|
||||
.idea/
|
||||
|
||||
+8
-1
@@ -32,7 +32,7 @@ This is a guide to have a common language to talk about "Talk".
|
||||
* Protected Profile: information about users that only moderators and admins can see
|
||||
|
||||
* Queue - Group of items based on a query, aka - moderation queue
|
||||
* Target - The item/s on which an action is performed..
|
||||
* Target - The item/s on which an action is performed
|
||||
|
||||
## Actions
|
||||
|
||||
@@ -64,3 +64,10 @@ Postmoderation means that comments appear on the site _before_ any moderation ac
|
||||
|
||||
* New comments appear in comment streams immediately.
|
||||
* New comments do not appear in moderation queues unless they are flagged by other users.
|
||||
|
||||
### Word lists
|
||||
|
||||
* Banned words - words that the site never allows in a comment
|
||||
* Suspect words - words whose usage needs to be approved by a moderator before being shown in the stream
|
||||
* Approved words - words that are usually Banned or Suspect sitewide, but approved for use in a specific article stream
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ if (app.get('env') !== 'test') {
|
||||
// APP MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
app.set('trust proxy', 'loopback');
|
||||
app.set('trust proxy', 1);
|
||||
app.use(helmet());
|
||||
app.use(bodyParser.json());
|
||||
app.use('/client', express.static(path.join(__dirname, 'dist')));
|
||||
@@ -97,7 +97,7 @@ app.use('/api', (err, req, res, next) => {
|
||||
res.status(err.status || 500);
|
||||
res.json({
|
||||
message: err.message,
|
||||
error: app.get('env') === 'development' ? err : null
|
||||
error: app.get('env') === 'development' ? err : {}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,7 +109,7 @@ app.use('/', (err, req, res, next) => {
|
||||
res.status(err.status || 500);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: app.get('env') === 'development' ? err : null
|
||||
error: app.get('env') === 'development' ? err : {}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,33 +13,30 @@ process.env.DEBUG = process.env.TALK_DEBUG;
|
||||
const app = require('../app');
|
||||
const debug = require('debug')('talk:server');
|
||||
const http = require('http');
|
||||
const initPromise = require('../init');
|
||||
const init = require('../init');
|
||||
const port = normalizePort(process.env.TALK_PORT || '3000');
|
||||
|
||||
let server;
|
||||
|
||||
initPromise
|
||||
.then(() => {
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
init().then(() => {
|
||||
|
||||
app.set('port', port);
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
server = http.createServer(app);
|
||||
|
||||
server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
});
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
});
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
|
||||
@@ -8,7 +8,7 @@ import CommunityContainer from 'containers/Community/CommunityContainer';
|
||||
import LayoutContainer from 'containers/LayoutContainer';
|
||||
|
||||
const routes = (
|
||||
<Route path='admin' component={LayoutContainer}>
|
||||
<Route path='/admin' component={LayoutContainer}>
|
||||
<IndexRoute component={ModerationQueue} />
|
||||
<Route path='embed' component={CommentStream} />
|
||||
<Route path='community' component={CommunityContainer} />
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as actions from '../constants/auth';
|
||||
import {base, handleResp, getInit} from '../helpers/response';
|
||||
|
||||
// Check Login
|
||||
|
||||
const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST});
|
||||
const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin});
|
||||
const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
|
||||
|
||||
export const checkLogin = () => dispatch => {
|
||||
dispatch(checkLoginRequest());
|
||||
fetch(`${base}/auth`, getInit('GET'))
|
||||
.then(handleResp)
|
||||
.then(user => {
|
||||
const isAdmin = !!user.roles.filter(i => i === 'admin').length;
|
||||
dispatch(checkLoginSuccess(user, isAdmin));
|
||||
})
|
||||
.catch(error => dispatch(checkLoginFailure(error)));
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
import React from 'react';
|
||||
import {Button, Icon} from 'react-mdl';
|
||||
import timeago from 'timeago.js';
|
||||
import styles from './CommentList.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
import Linkify from 'react-linkify';
|
||||
import {FabButton} from 'coral-ui';
|
||||
|
||||
const linkify = new Linkify();
|
||||
|
||||
@@ -14,7 +14,7 @@ export default props => {
|
||||
const links = linkify.getMatches(props.comment.get('body'));
|
||||
|
||||
return (
|
||||
<li tabindex={props.index} className={`${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<li tabIndex={props.index} className={`${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<i className={`material-icons ${styles.avatar}`}>person</i>
|
||||
@@ -26,12 +26,12 @@ export default props => {
|
||||
{links ?
|
||||
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
<div className={styles.actions}>
|
||||
{props.actions.map(action => canShowAction(action, props.comment) ? (
|
||||
<Button className={styles.actionButton}
|
||||
{props.actions.map((action, i) => canShowAction(action, props.comment) ? (
|
||||
<FabButton icon={props.actionsMap[action].icon} className={styles.actionButton}
|
||||
cStyle={action}
|
||||
key={i}
|
||||
onClick={() => props.onClickAction(props.actionsMap[action].status, props.comment.get('id'))}
|
||||
fab colored>
|
||||
<Icon name={props.actionsMap[action].icon} />
|
||||
</Button>
|
||||
/>
|
||||
) : null)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -112,15 +112,15 @@ export default class CommentList extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {singleView, commentIds, comments, hideActive} = this.props;
|
||||
const {singleView, commentIds, comments, hideActive, key} = this.props;
|
||||
const {active} = this.state;
|
||||
|
||||
return (
|
||||
<ul className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
|
||||
<ul className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}>
|
||||
{commentIds.map((commentId, index) => (
|
||||
<Comment comment={comments.get(commentId)}
|
||||
ref={el => { if (el && commentId === active) { this._active = el; } }}
|
||||
key={`${index }comment`}
|
||||
key={index}
|
||||
index={index}
|
||||
onClickAction={this.onClickAction}
|
||||
actions={this.props.actions}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Layout, Navigation, Drawer, Header} from 'react-mdl';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
// App header. If we add a navbar it should be here
|
||||
export default (props) => (
|
||||
<Layout fixedDrawer>
|
||||
<Header title='Talk'>
|
||||
<Navigation>
|
||||
<Link className={styles.navLink} to={'/admin/'}>{lang.t('configure.moderate')}</Link>
|
||||
<Link className={styles.navLink} to={'/admin/configure'}>{lang.t('Configure')}</Link>
|
||||
</Navigation>
|
||||
</Header>
|
||||
<Drawer>
|
||||
<Navigation>
|
||||
<Link className={styles.navLink} to={'/admin/'}>{lang.t('configure.moderate')}</Link>
|
||||
<Link className={styles.navLink} to={'/admin/configure'}>{lang.t('configure.Configure')}</Link>
|
||||
</Navigation>
|
||||
</Drawer>
|
||||
{props.children}
|
||||
</Layout>
|
||||
);
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -0,0 +1,12 @@
|
||||
.layout {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.layout img {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import {Layout} from 'react-mdl';
|
||||
import styles from './NotFound.css';
|
||||
|
||||
export const NotFound = () => (
|
||||
<Layout fixedDrawer>
|
||||
<div className={styles.layout} >
|
||||
<h1>Page Not Found</h1>
|
||||
<p>The communicorn feels your pain.</p>
|
||||
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import {Layout} from 'react-mdl';
|
||||
import styles from './NotFound.css';
|
||||
|
||||
export const PermissionRequired = () => (
|
||||
<Layout fixedDrawer>
|
||||
<div className={styles.layout} >
|
||||
<h1>Permission Required</h1>
|
||||
<p>We’re sorry, but you don’t have access to that page.</p>
|
||||
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
.header {
|
||||
background: #505050;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header > div {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
width: 1170px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.active {
|
||||
background: #232323;
|
||||
}
|
||||
|
||||
.version {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import React from 'react';
|
||||
import {Navigation, Header} from 'react-mdl';
|
||||
import {Link} from 'react-router';
|
||||
import {Link, IndexLink} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import {Logo} from './Logo';
|
||||
|
||||
export default () => (
|
||||
<Header title='Talk'>
|
||||
<Header className={styles.header}>
|
||||
<Logo />
|
||||
<Navigation>
|
||||
<Link className={styles.navLink} to="/admin">{lang.t('configure.moderate')}</Link>
|
||||
<Link className={styles.navLink} to="/admin/community">{lang.t('configure.community')}</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure">{lang.t('configure.configure')}</Link>
|
||||
<span>
|
||||
{`v${process.env.VERSION}`}
|
||||
</span>
|
||||
<IndexLink className={styles.navLink} to="/admin" activeClassName={styles.active}>{lang.t('configure.moderate')}</IndexLink>
|
||||
<Link className={styles.navLink} to="/admin/community" activeClassName={styles.active}>{lang.t('configure.community')}</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure" activeClassName={styles.active}>{lang.t('configure.configure')}</Link>
|
||||
</Navigation>
|
||||
<div className={styles.version}>
|
||||
{`v${process.env.VERSION}`}
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.logo h1 {
|
||||
color: #272727;
|
||||
font-size: 20px;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.logo {
|
||||
background: #E5E5E5;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import styles from './Logo.css';
|
||||
import {CoralLogo} from 'coral-ui';
|
||||
|
||||
export const Logo = () => (
|
||||
<div className={styles.logo}>
|
||||
<h1>
|
||||
<CoralLogo stroke="#E5E5E5" />
|
||||
<span>Talk</span>
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
|
||||
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
|
||||
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
|
||||
|
||||
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
|
||||
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
|
||||
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
|
||||
@@ -1,19 +1,41 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {Layout} from '../components/ui/Layout';
|
||||
import {checkLogin} from '../actions/auth';
|
||||
import {NotFound} from '../components/NotFound';
|
||||
import {PermissionRequired} from '../components/PermissionRequired';
|
||||
|
||||
class LayoutContainer extends Component {
|
||||
componentWillMount () {
|
||||
this.props.checkLogin();
|
||||
}
|
||||
render () {
|
||||
const {isAdmin, loggedIn} = this.props.auth;
|
||||
|
||||
if (!loggedIn) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
if (!isAdmin && loggedIn) {
|
||||
return <PermissionRequired />;
|
||||
}
|
||||
|
||||
return <Layout {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
LayoutContainer.propTypes = {};
|
||||
|
||||
const mapStateToProps = () => ({});
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({dispatch});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
checkLogin: () => dispatch(checkLogin()),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(LayoutContainer);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.listContainer {
|
||||
@@ -6,8 +5,13 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tabBar {
|
||||
background: #9E9E9E;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (--big-viewport) {
|
||||
|
||||
@@ -61,9 +61,9 @@ class ModerationQueue extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
<div className='mdl-tabs mdl-js-tabs mdl-js-ripple-effect'>
|
||||
<div className='mdl-tabs__tab-bar'>
|
||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||
<a href='#pending' onClick={() => this.onTabClick('pending')}
|
||||
className={`mdl-tabs__tab is-active ${styles.tab}`}>{lang.t('modqueue.pending')}</a>
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.pending')}</a>
|
||||
<a href='#rejected' onClick={() => this.onTabClick('rejected')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.rejected')}</a>
|
||||
<a href='#flagged' onClick={() => this.onTabClick('flagged')}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
export const base = '/api/v1';
|
||||
|
||||
export const getInit = (method, body) => {
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
});
|
||||
let init = {
|
||||
method,
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
const init = {method, headers};
|
||||
if (method.toLowerCase() !== 'get') {
|
||||
init.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import {Map} from 'immutable';
|
||||
import * as actions from '../constants/auth';
|
||||
|
||||
const initialState = Map({
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
isAdmin: false
|
||||
});
|
||||
|
||||
export default function auth (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return state
|
||||
.set('loggedIn', false)
|
||||
.set('user', null);
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', true)
|
||||
.set('isAdmin', action.isAdmin)
|
||||
.set('user', action.user);
|
||||
case actions.LOGOUT_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', false)
|
||||
.set('user', null);
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ import {combineReducers} from 'redux';
|
||||
import comments from 'reducers/comments';
|
||||
import settings from 'reducers/settings';
|
||||
import community from 'reducers/community';
|
||||
import auth from 'reducers/auth';
|
||||
|
||||
// Combine all reducers into a main one
|
||||
export default combineReducers({
|
||||
settings,
|
||||
comments,
|
||||
community
|
||||
community,
|
||||
auth
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,11 @@ export default store => next => action => {
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
const fetchModerationQueueComments = store =>
|
||||
|
||||
Promise.all([fetch('/api/v1/queue/comments/pending'), fetch('/api/v1/comments/status/rejected'), fetch('/api/v1/comments/action/flag')])
|
||||
Promise.all([
|
||||
fetch('/api/v1/queue/comments/pending'),
|
||||
fetch('/api/v1/comments?status=rejected'),
|
||||
fetch('/api/v1/comments?action=flag')
|
||||
])
|
||||
.then(res => Promise.all(res.map(r => r.json())))
|
||||
.then(res => {
|
||||
res[2] = res[2].map(comment => { comment.flagged = true; return comment; });
|
||||
@@ -48,7 +52,7 @@ Promise.all([fetch('/api/v1/queue/comments/pending'), fetch('/api/v1/comments/st
|
||||
|
||||
const updateComment = (store, comment) => {
|
||||
fetch(`/api/v1/comments/${comment.get('id')}/status`, {
|
||||
method: 'POST',
|
||||
method: 'PUT',
|
||||
headers: jsonHeader,
|
||||
body: JSON.stringify({status: comment.get('status')})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -61,8 +61,8 @@ class CommentStream extends Component {
|
||||
// Set up messaging between embedded Iframe an parent component
|
||||
// Using recommended Pym init code which violates .eslint standards
|
||||
const pym = new Pym.Child({polling: 100});
|
||||
const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl)[1];
|
||||
this.props.getStream(path);
|
||||
const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl);
|
||||
this.props.getStream(path && path[1] || window.location);
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -84,8 +84,9 @@ class CommentStream extends Component {
|
||||
|
||||
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
|
||||
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
|
||||
const {loggedIn, user} = this.props.auth;
|
||||
return <div>
|
||||
const {actions, users, comments} = this.props.items;
|
||||
const {loggedIn, user, showSignInDialog} = this.props.auth;
|
||||
return <div className={showSignInDialog ? 'expandForSignin' : ''}>
|
||||
{
|
||||
rootItem
|
||||
? <div>
|
||||
@@ -105,16 +106,16 @@ class CommentStream extends Component {
|
||||
id={rootItemId}
|
||||
premod={this.props.config.moderation}
|
||||
reply={false}
|
||||
canPost={loggedIn}
|
||||
author={user}
|
||||
/>
|
||||
{!loggedIn && <SignInContainer />}
|
||||
</div>
|
||||
{
|
||||
rootItem.comments && rootItem.comments.map((commentId) => {
|
||||
const comment = this.props.items.comments[commentId];
|
||||
const comment = comments[commentId];
|
||||
return <div className="comment" key={commentId}>
|
||||
<hr aria-hidden={true}/>
|
||||
<AuthorName name={comment.username}/>
|
||||
<AuthorName author={users[comment.author_id]}/>
|
||||
<PubDate created_at={comment.created_at}/>
|
||||
<Content body={comment.body}/>
|
||||
<div className="commentActionsLeft">
|
||||
@@ -124,7 +125,7 @@ class CommentStream extends Component {
|
||||
<LikeButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
like={this.props.items.actions[comment.like]}
|
||||
like={actions[comment.like]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
@@ -135,7 +136,7 @@ class CommentStream extends Component {
|
||||
<FlagButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
flag={this.props.items.actions[comment.flag]}
|
||||
flag={actions[comment.flag]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
@@ -151,6 +152,7 @@ class CommentStream extends Component {
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
id={rootItemId}
|
||||
author={user}
|
||||
parent_id={commentId}
|
||||
premod={this.props.config.moderation}
|
||||
showReply={comment.showReply}/>
|
||||
@@ -160,13 +162,13 @@ class CommentStream extends Component {
|
||||
let reply = this.props.items.comments[replyId];
|
||||
return <div className="reply" key={replyId}>
|
||||
<hr aria-hidden={true}/>
|
||||
<AuthorName name={reply.username}/>
|
||||
<AuthorName author={users[comment.author_id]}/>
|
||||
<PubDate created_at={reply.created_at}/>
|
||||
<Content body={reply.body}/>
|
||||
<div className="replyActionsLeft">
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={replyId}/>
|
||||
parent_id={reply.parent_id}/>
|
||||
<LikeButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
@@ -188,8 +190,8 @@ class CommentStream extends Component {
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<PermalinkButton
|
||||
comment_id={reply.comment_id}
|
||||
asset_id={reply.comment_id}
|
||||
comment_id={reply.parent_id}
|
||||
asset_id={rootItemId}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -3,8 +3,12 @@ body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
min-height: 700px;
|
||||
margin: 0px;
|
||||
padding: 0px 0px 50px 0px;
|
||||
}
|
||||
|
||||
.expandForSignin {
|
||||
min-height: 550px;
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@@ -3,6 +3,7 @@ import translations from './../translations';
|
||||
const lang = new I18n(translations);
|
||||
import * as actions from '../constants/auth';
|
||||
import {base, handleResp, getInit} from '../helpers/response';
|
||||
import {addItem} from './items';
|
||||
|
||||
// Dialog Actions
|
||||
export const showSignInDialog = () => ({type: actions.SHOW_SIGNIN_DIALOG});
|
||||
@@ -19,8 +20,8 @@ export const cleanState = () => ({type: actions.CLEAN_STATE});
|
||||
// Sign In Actions
|
||||
|
||||
const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST});
|
||||
const signInSuccess = (user) => ({type: actions.FETCH_SIGNIN_SUCCESS, user});
|
||||
const signInFailure = (error) => ({type: actions.FETCH_SIGNIN_FAILURE, error});
|
||||
const signInSuccess = user => ({type: actions.FETCH_SIGNIN_SUCCESS, user});
|
||||
const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
|
||||
|
||||
export const fetchSignIn = (formData) => dispatch => {
|
||||
dispatch(signInRequest());
|
||||
@@ -29,6 +30,7 @@ export const fetchSignIn = (formData) => dispatch => {
|
||||
.then(({user}) => {
|
||||
dispatch(hideSignInDialog());
|
||||
dispatch(signInSuccess(user));
|
||||
dispatch(addItem(user, 'users'));
|
||||
})
|
||||
.catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError'))));
|
||||
};
|
||||
@@ -54,8 +56,10 @@ export const facebookCallback = (err, data) => dispatch => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
dispatch(signInFacebookSuccess(JSON.parse(data)));
|
||||
const user = JSON.parse(data);
|
||||
dispatch(signInFacebookSuccess(user));
|
||||
dispatch(hideSignInDialog());
|
||||
dispatch(addItem(user, 'users'));
|
||||
} catch (err) {
|
||||
dispatch(signInFacebookFailure(err));
|
||||
return;
|
||||
@@ -87,9 +91,9 @@ const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUE
|
||||
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'))
|
||||
export const fetchForgotPassword = email => dispatch => {
|
||||
dispatch(forgotPassowordRequest(email));
|
||||
fetch(`${base}/user/request-password-reset`, getInit('POST', {email}))
|
||||
.then(handleResp)
|
||||
.then(() => dispatch(forgotPassowordSuccess()))
|
||||
.catch(error => dispatch(forgotPassowordFailure(error)));
|
||||
@@ -114,3 +118,16 @@ export const logout = () => dispatch => {
|
||||
export const validForm = () => ({type: actions.VALID_FORM});
|
||||
export const invalidForm = error => ({type: actions.INVALID_FORM, error});
|
||||
|
||||
// Check Login
|
||||
|
||||
const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST});
|
||||
const checkLoginSuccess = user => ({type: actions.CHECK_LOGIN_SUCCESS, user});
|
||||
const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
|
||||
|
||||
export const checkLogin = () => dispatch => {
|
||||
dispatch(checkLoginRequest());
|
||||
fetch(`${base}/auth`, getInit('GET'))
|
||||
.then(handleResp)
|
||||
.then(user => dispatch(checkLoginSuccess(user)))
|
||||
.catch(error => dispatch(checkLoginFailure(error)));
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ const getInit = (method, body) => {
|
||||
};
|
||||
|
||||
const init = {method, headers};
|
||||
if (method.toLowerCase() !== 'get') {
|
||||
if (body) {
|
||||
init.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ const getInit = (method, body) => {
|
||||
};
|
||||
|
||||
const responseHandler = response => {
|
||||
if (response.status === 204) {
|
||||
return;
|
||||
}
|
||||
return response.ok ? response.json() : Promise.reject(`${response.status} ${response.statusText}`);
|
||||
};
|
||||
/**
|
||||
@@ -103,8 +106,16 @@ export function getStream (assetUrl) {
|
||||
/* Add items to the store */
|
||||
const itemTypes = Object.keys(json);
|
||||
for (let i = 0; i < itemTypes.length; i++ ) {
|
||||
for (let j = 0; j < json[itemTypes[i]].length; j++ ) {
|
||||
dispatch(addItem(json[itemTypes[i]][j], itemTypes[i]));
|
||||
if (itemTypes[i] === 'actions') {
|
||||
for (let j = 0; j < json[itemTypes[i]].length; j++ ) {
|
||||
let action = json[itemTypes[i]][j];
|
||||
action.id = `${action.action_type}_${action.item_id}`;
|
||||
dispatch(addItem(action, 'actions'));
|
||||
}
|
||||
} else {
|
||||
for (let j = 0; j < json[itemTypes[i]].length; j++ ) {
|
||||
dispatch(addItem(json[itemTypes[i]][j], itemTypes[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,8 +210,6 @@ export function postItem (item, type, id) {
|
||||
};
|
||||
}
|
||||
|
||||
//http://localhost:16180/v1/action/flag/user/user_89654/on/item/87e418c5-aafb-4eb7-9ce4-78f28793782a
|
||||
|
||||
/*
|
||||
* PostAction
|
||||
* Posts an action to an item
|
||||
@@ -243,14 +252,9 @@ export function postAction (item_id, action_type, user_id, item_type) {
|
||||
*
|
||||
*/
|
||||
|
||||
export function deleteAction (item_id, action_type, user_id, item_type) {
|
||||
export function deleteAction (action_id) {
|
||||
return () => {
|
||||
const action = {
|
||||
action_type,
|
||||
user_id
|
||||
};
|
||||
|
||||
return fetch(`/api/v1/${item_type}/${item_id}/actions`, getInit('DELETE', action))
|
||||
return fetch(`/api/v1/actions/${action_id}`, {method: 'DELETE'})
|
||||
.then(responseHandler);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,3 +27,7 @@ export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
|
||||
export const INVALID_FORM = 'INVALID_FORM';
|
||||
export const VALID_FORM = 'VALID_FORM';
|
||||
|
||||
export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
|
||||
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
|
||||
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ const initialState = Map({
|
||||
showSignInDialog: false,
|
||||
view: 'SIGNIN',
|
||||
error: '',
|
||||
passwordRequestSuccess: null,
|
||||
passwordRequestFailure: null,
|
||||
successSignUp: false
|
||||
});
|
||||
|
||||
@@ -22,6 +24,8 @@ export default function auth (state = initialState, action) {
|
||||
showSignInDialog: false,
|
||||
view: 'SIGNIN',
|
||||
error: '',
|
||||
passwordRequestFailure: null,
|
||||
passwordRequestSuccess: null,
|
||||
successSignUp: false
|
||||
}));
|
||||
case actions.CHANGE_VIEW :
|
||||
@@ -33,6 +37,14 @@ export default function auth (state = initialState, action) {
|
||||
case actions.FETCH_SIGNIN_REQUEST:
|
||||
return state
|
||||
.set('isLoading', true);
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return state
|
||||
.set('loggedIn', false)
|
||||
.set('user', null);
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', true)
|
||||
.set('user', action.user);
|
||||
case actions.FETCH_SIGNIN_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', true)
|
||||
@@ -40,7 +52,8 @@ export default function auth (state = initialState, action) {
|
||||
case actions.FETCH_SIGNIN_FAILURE:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('error', action.error);
|
||||
.set('error', action.error)
|
||||
.set('user', null);
|
||||
case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
|
||||
return state
|
||||
.set('user', action.user)
|
||||
@@ -70,6 +83,14 @@ export default function auth (state = initialState, action) {
|
||||
case actions.VALID_FORM:
|
||||
return state
|
||||
.set('error', '');
|
||||
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
|
||||
return state
|
||||
.set('passwordRequestFailure', null)
|
||||
.set('passwordRequestSuccess', 'If you have a registered account, a password reset link was sent to that email');
|
||||
case actions.FETCH_FORGOT_PASSWORD_FAILURE:
|
||||
return state
|
||||
.set('passwordRequestFailure', 'There was an error sending your password reset email. Please try again soon!')
|
||||
.set('passwordRequestSuccess', null);
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
const packagename = 'coral-plugin-author-name';
|
||||
|
||||
const AuthorName = ({name}) =>
|
||||
const AuthorName = ({author}) =>
|
||||
<div className={`${packagename}-text`}>
|
||||
{name}
|
||||
{author && author.displayName}
|
||||
</div>;
|
||||
|
||||
export default AuthorName;
|
||||
|
||||
@@ -12,7 +12,8 @@ class CommentBox extends Component {
|
||||
id: PropTypes.string,
|
||||
comments: PropTypes.array,
|
||||
reply: PropTypes.bool,
|
||||
canPost: PropTypes.bool
|
||||
canPost: PropTypes.bool,
|
||||
currentUser: PropTypes.object
|
||||
}
|
||||
|
||||
state = {
|
||||
@@ -21,11 +22,11 @@ class CommentBox extends Component {
|
||||
}
|
||||
|
||||
postComment = () => {
|
||||
const {postItem, updateItem, id, parent_id, addNotification, appendItemArray, premod} = this.props;
|
||||
const {postItem, updateItem, id, parent_id, addNotification, appendItemArray, premod, author} = this.props;
|
||||
let comment = {
|
||||
body: this.state.body,
|
||||
asset_id: id,
|
||||
username: this.state.username
|
||||
author_id: author.id
|
||||
};
|
||||
let related;
|
||||
let parent_type;
|
||||
@@ -52,7 +53,7 @@ class CommentBox extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {styles, reply, canPost} = this.props;
|
||||
const {styles, reply, author} = this.props;
|
||||
// How to handle language in plugins? Should we have a dependency on our central translation file?
|
||||
return <div>
|
||||
<div
|
||||
@@ -73,7 +74,7 @@ class CommentBox extends Component {
|
||||
rows={3}/>
|
||||
</div>
|
||||
<div className={`${name}-button-container`}>
|
||||
{ canPost && (
|
||||
{ author && (
|
||||
<button
|
||||
className={`${name}-button`}
|
||||
style={styles && styles.button}
|
||||
|
||||
@@ -4,18 +4,22 @@ import translations from './translations.json';
|
||||
|
||||
const name = 'coral-plugin-flags';
|
||||
|
||||
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification}) => {
|
||||
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification, currentUser}) => {
|
||||
const flagged = flag && flag.current_user;
|
||||
const onFlagClick = () => {
|
||||
if (!currentUser) {
|
||||
return;
|
||||
}
|
||||
if (!flagged) {
|
||||
postAction(id, 'flag', '123', 'comments')
|
||||
postAction(id, 'flag', currentUser.id, 'comments')
|
||||
.then((action) => {
|
||||
addItem({...action, current_user:true}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, action.id, 'comments');
|
||||
let id = `${action.action_type}_${action.item_id}`;
|
||||
addItem({id, current_user: action, count: flag ? flag.count + 1 : 1}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, id, 'comments');
|
||||
});
|
||||
addNotification('success', lang.t('flag-notif'));
|
||||
} else {
|
||||
deleteAction(id, 'flag', '123', 'comments')
|
||||
deleteAction(flagged.id)
|
||||
.then(() => {
|
||||
updateItem(id, 'flag', '', 'comments');
|
||||
});
|
||||
@@ -31,7 +35,7 @@ const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, ad
|
||||
: <span className={`${name}-button-text`}>{lang.t('flag')}</span>
|
||||
}
|
||||
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
|
||||
style={flagged && styles.flaggedIcon}
|
||||
style={flagged ? styles.flaggedIcon : {}}
|
||||
aria-hidden={true}>flag</i>
|
||||
</button>
|
||||
</div>;
|
||||
|
||||
@@ -4,17 +4,21 @@ import translations from './translations.json';
|
||||
|
||||
const name = 'coral-plugin-flags';
|
||||
|
||||
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem}) => {
|
||||
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem, currentUser}) => {
|
||||
const liked = like && like.current_user;
|
||||
const onLikeClick = () => {
|
||||
if (!currentUser) {
|
||||
return;
|
||||
}
|
||||
if (!liked) {
|
||||
postAction(id, 'like', '123', 'comments')
|
||||
postAction(id, 'like', currentUser.id, 'comments')
|
||||
.then((action) => {
|
||||
addItem({id: action.id, current_user:true, count: like ? like.count + 1 : 1}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, action.id, 'comments');
|
||||
let id = `${action.action_type}_${action.item_id}`;
|
||||
addItem({id, current_user: action, count: like ? like.count + 1 : 1}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, id, 'comments');
|
||||
});
|
||||
} else {
|
||||
deleteAction(id, 'like', '123', 'comments')
|
||||
deleteAction(liked.id)
|
||||
.then(() => {
|
||||
updateItem(like.id, 'count', like.count - 1, 'actions');
|
||||
updateItem(like.id, 'current_user', false, 'actions');
|
||||
|
||||
@@ -5,25 +5,56 @@ 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" />
|
||||
class ForgotContent extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault();
|
||||
this.props.fetchForgotPassword(this.emailInput.value);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {changeView, auth} = this.props;
|
||||
const {passwordRequestSuccess, passwordRequestFailure} = auth;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h1>{lang.t('signIn.recoverPassword')}</h1>
|
||||
</div>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className={styles.formField}>
|
||||
<label htmlFor="email">{lang.t('signIn.email')}</label>
|
||||
<input
|
||||
ref={input => this.emailInput = input}
|
||||
type="text"
|
||||
id="email"
|
||||
name="email" />
|
||||
</div>
|
||||
<Button type="submit" cStyle="black" className={styles.signInButton}>
|
||||
{lang.t('signIn.recoverPassword')}
|
||||
</Button>
|
||||
{
|
||||
passwordRequestSuccess
|
||||
? <p className={styles.passwordRequestSuccess}>{passwordRequestSuccess}</p>
|
||||
: null
|
||||
}
|
||||
{
|
||||
passwordRequestFailure
|
||||
? <p className={styles.attention}>{passwordRequestFailure}</p>
|
||||
: null
|
||||
}
|
||||
</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>
|
||||
<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;
|
||||
|
||||
@@ -106,7 +106,7 @@ input.error{
|
||||
.userBox a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.attention {
|
||||
@@ -128,4 +128,15 @@ input.error{
|
||||
|
||||
.action {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
border: 1px solid green;
|
||||
background-color: lightgreen;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.passwordRequestFailure {
|
||||
border: 1px solid orange;
|
||||
background-color: 1px solid coral
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
fetchForgotPassword,
|
||||
facebookCallback,
|
||||
invalidForm,
|
||||
validForm
|
||||
validForm,
|
||||
checkLogin
|
||||
} from '../../coral-framework/actions/auth';
|
||||
|
||||
class SignInContainer extends Component {
|
||||
@@ -43,6 +44,10 @@ class SignInContainer extends Component {
|
||||
this.addError = this.addError.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.checkLogin();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.authCallback = this.props.facebookCallback;
|
||||
const {formData} = this.state;
|
||||
@@ -147,6 +152,7 @@ const mapStateToProps = state => ({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
checkLogin: () => dispatch(checkLogin()),
|
||||
facebookCallback: (err, data) => dispatch(facebookCallback(err, data)),
|
||||
fetchSignUp: formData => dispatch(fetchSignUp(formData)),
|
||||
fetchSignIn: formData => dispatch(fetchSignIn(formData)),
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
|
||||
const CoralLogo = ({height = '30px', width = '30px', stroke = '#FFFFFF'}) => (
|
||||
<svg width={width} height={height} viewBox='0 0 381 391' version='1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink'>
|
||||
<g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
|
||||
<g id='Wordmark-Round' transform='translate(-1833.000000, -411.000000)' stroke={stroke} strokeWidth='22' strokeLinecap='round' strokeLinejoin='round'>
|
||||
<g id='coralProjectLogo-2-Copy-2' transform='translate(1842.000000, 421.000000)'>
|
||||
<g id='Layer_2' transform='translate(2.268750, 1.133903)'>
|
||||
<rect id='Rectangle-1' fill='#F47E6B' x='0' y='0' width='358.4625' height='368.518519' rx='40'>
|
||||
</rect>
|
||||
<path d='M0.226875,105.679772 C13.7259375,122.688319 41.29125,131.986325 56.71875,116.792023 C67.0415625,106.586895 62.5040625,93.0934473 76.910625,63.3851852 C83.94375,48.7578348 90.523125,38.3259259 101.980313,37.5321937 C115.139063,36.6250712 131.814375,50.3452991 134.31,69.3948718 C137.826563,95.1344729 115.479375,105.339601 121.15125,123.822222 C127.390313,144.119088 157.110938,140.377208 166.52625,165.096296 C170.042813,174.394302 171.630938,190.382336 163.463438,198.319658 C149.170313,212.266667 120.584063,186.073504 80.7675,198.319658 C73.280625,200.587464 56.3784375,205.803419 51.500625,219.523647 C46.73625,233.130484 55.584375,251.046154 67.60875,260.797721 C93.245625,281.661538 119.79,254.674644 159.379688,271.909972 C181.840313,281.661538 203.053125,303.319088 208.725,330.305983 C211.674375,344.593162 209.6325,356.952707 207.704063,364.549858' id='Shape'>
|
||||
</path>
|
||||
</g>
|
||||
<g id='Layer_3' transform='translate(43.106250, 289.145299)'>
|
||||
<path d='M90.4096875,72.5698006 C78.8390625,41.3874644 64.9996875,31.4091168 54.1096875,28.234188 C41.8584375,24.7190883 30.7415625,29.0279202 17.8096875,21.2039886 C8.394375,15.4210826 3.403125,6.57663818 0.680625,0'id='Shape'></path>
|
||||
</g><g id='Layer_4' transform='translate(220.068750, 209.772080)'>
|
||||
<path d='M81.7884375,152.963533 C74.9821875,122.007977 61.8234375,104.77265 50.593125,94.5675214 C31.8759375,77.6723647 14.746875,77.1054131 5.218125,57.2621083 C4.4240625,55.5612536 -5.7853125,33.4501425 5.218125,17.008547 C14.52,3.06153846 33.350625,1.47407407 41.518125,0.907122507 C64.3190625,-0.907122507 73.280625,9.52478632 100.959375,13.039886 C117.180938,15.0809117 130.793438,13.4934473 139.30125,12.0193732'id='Shape'></path>
|
||||
</g>
|
||||
<g id='Layer_5' transform='translate(285.862500, 289.145299)'>
|
||||
<path d='M74.415,2.04102564 C66.3609375,0.793732194 51.046875,-0.566951567 33.12375,5.1025641 C17.5828125,9.97834758 6.80625,18.0290598 0.9075,23.2450142'id='Shape'></path>
|
||||
</g>
|
||||
<g id='Layer_6' transform='translate(174.693750, 1.133903)'>
|
||||
<path d='M184.109063,151.035897 C170.950313,174.507692 158.699063,180.290598 149.850938,181.311111 C133.85625,183.011966 129.091875,168.724786 104.475938,163.168661 C79.2928125,157.499145 72.2596875,169.745299 52.0678125,163.168661 C30.0609375,155.911681 18.7171875,134.821083 12.705,123.822222 C-1.588125,97.4022792 -0.3403125,71.6626781 0.5671875,51.2524217 C1.588125,29.4814815 6.125625,12.0193732 9.6421875,0.907122507'id='Shape'></path>
|
||||
<path d='M183.541875,69.3948718 C170.496563,56.6951567 157.564688,45.8096866 149.28375,39.0062678 C143.385,34.1304843 134.990625,33.2233618 128.297813,36.8518519 C128.070938,36.9652422 127.844063,37.0786325 127.730625,37.1920228 C122.739375,40.1401709 119.449688,45.3561254 118.8825,51.1390313 C118.201875,59.0763533 117.0675,71.4358974 114.912188,82.8883191 C114.118125,87.0837607 107.085,103.865527 89.7290625,110.668946 C77.2509375,115.544729 62.0503125,110.668946 53.4290625,101.597721 C40.38375,87.7641026 44.8078125,66.9002849 47.416875,55.2210826 C53.5425,26.760114 73.3940625,9.07122507 82.6959375,1.81424501'id='Shape'></path>
|
||||
</g>
|
||||
<g id='Layer_7' transform='translate(3.403125, 179.156695)'>
|
||||
<path d='M0.1134375,0.226780627 C2.949375,6.34985755 8.394375,16.1014245 18.2634375,25.3994302 C27.67875,34.2438746 37.3209375,39.0062678 43.4465625,41.5008547'id='Shape'></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
CoralLogo.propTypes = {
|
||||
height: PropTypes.string,
|
||||
width: PropTypes.string,
|
||||
stroke: PropTypes.string
|
||||
};
|
||||
|
||||
export default CoralLogo;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.type--approve {
|
||||
background: #00796b;
|
||||
color: rgba(255, 255, 255, 0.901961);
|
||||
}
|
||||
|
||||
.type--reject {
|
||||
background: #d32f2f ;
|
||||
color: rgba(255, 255, 255, 0.901961);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import styles from './FabButton.css';
|
||||
import {FABButton, Icon} from 'react-mdl';
|
||||
|
||||
const FabButton = ({cStyle = 'local', icon, className, ...props}) => (
|
||||
<FABButton className={`${styles[`type--${cStyle}`]} ${className ? className : ''}`} {...props}>
|
||||
<Icon name={icon} />
|
||||
</FABButton>
|
||||
);
|
||||
|
||||
export default FabButton;
|
||||
@@ -1 +1,3 @@
|
||||
export {default as Dialog} from './components/Dialog';
|
||||
export {default as CoralLogo} from './components/CoralLogo';
|
||||
export {default as FabButton} from './components/FabButton';
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
const Setting = require('./models/setting');
|
||||
const wordlist = require('./services/wordlist');
|
||||
|
||||
const defaults = {id: '1', moderation: 'pre'};
|
||||
module.exports = Setting.init(defaults);
|
||||
module.exports = () => Promise.all([
|
||||
|
||||
// presumably this file will grow, which is why I've broken it out.
|
||||
// Upsert the settings object.
|
||||
Setting
|
||||
.init({id: '1', moderation: 'pre'})
|
||||
.then(() => {
|
||||
|
||||
// Load in the wordlist now that settings have been init'd.
|
||||
return wordlist.init();
|
||||
})
|
||||
|
||||
]);
|
||||
|
||||
+52
-27
@@ -42,31 +42,58 @@ ActionSchema.statics.findByItemIdArray = function(item_ids) {
|
||||
* @param {String} ids array of user identifiers (uuid)
|
||||
*/
|
||||
ActionSchema.statics.getActionSummaries = function(item_ids) {
|
||||
return ActionSchema.statics.findByItemIdArray(item_ids).then((rawActions) => {
|
||||
// Create an object with a count of each action type for each item
|
||||
const actionSummaries = rawActions.reduce((actionObj, action) => {
|
||||
if (!actionObj[action.item_id]) {
|
||||
actionObj[action.item_id] = {
|
||||
id: action.id,
|
||||
item_type: action.item_type,
|
||||
action_type: action.action_type,
|
||||
count: 1,
|
||||
current_user: false //Update this later when we have authentication
|
||||
};
|
||||
} else {
|
||||
actionObj[action.item_id].count ++;
|
||||
}
|
||||
return actionObj;
|
||||
}, {});
|
||||
return Action.aggregate([
|
||||
{
|
||||
|
||||
// Return an array extracted from the actionSummaries object
|
||||
return Object.keys(actionSummaries).reduce((actions, key) => {
|
||||
let actionSummary = actionSummaries[key];
|
||||
actionSummary.item_id = key;
|
||||
actions.push(actionSummary);
|
||||
return actions;
|
||||
}, []);
|
||||
});
|
||||
// only grab items that match the specified item id's
|
||||
$match: {
|
||||
item_id: {
|
||||
$in: item_ids
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
|
||||
// group unique documents by these properties, we are leveraging the
|
||||
// fact that each uuid is completly unique.
|
||||
_id: {
|
||||
item_id: '$item_id',
|
||||
action_type: '$action_type'
|
||||
},
|
||||
|
||||
// and sum up all actions matching the above grouping criteria
|
||||
count: {
|
||||
$sum: 1
|
||||
},
|
||||
|
||||
// we are leveraging the fact that each uuid is completly unique and
|
||||
// just grabbing the last instance of the item type here.
|
||||
item_type: {
|
||||
$last: '$item_type'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
|
||||
// suppress the _id field
|
||||
_id: false,
|
||||
|
||||
// map the fields from the _id grouping down a level
|
||||
item_id: '$_id.item_id',
|
||||
action_type: '$_id.action_type',
|
||||
|
||||
// map the field directly
|
||||
count: '$count',
|
||||
item_type: '$item_type',
|
||||
|
||||
// set the current user to false here
|
||||
current_user: {$literal: false}
|
||||
}
|
||||
}
|
||||
])
|
||||
.exec();
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -90,9 +117,7 @@ ActionSchema.statics.findCommentsIdByActionType = function(action_type, item_typ
|
||||
return Action.find({
|
||||
'action_type': action_type,
|
||||
'item_type': item_type
|
||||
},
|
||||
'item_id'
|
||||
);
|
||||
}, 'item_id');
|
||||
};
|
||||
|
||||
const Action = mongoose.model('Action', ActionSchema);
|
||||
|
||||
+44
-36
@@ -17,7 +17,6 @@ const CommentSchema = new Schema({
|
||||
},
|
||||
asset_id: String,
|
||||
author_id: String,
|
||||
username: String,
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['accepted', 'rejected', ''],
|
||||
@@ -31,19 +30,6 @@ const CommentSchema = new Schema({
|
||||
}
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// New Statics
|
||||
//==============================================================================
|
||||
|
||||
/**
|
||||
* Create a comment.
|
||||
* @param {String} body content of comment
|
||||
*/
|
||||
CommentSchema.statics.new = function(body, author_id, asset_id, parent_id, status, username) {
|
||||
let comment = new Comment({body, author_id, asset_id, parent_id, status, username});
|
||||
return comment.save();
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// Find Statics
|
||||
//==============================================================================
|
||||
@@ -51,7 +37,8 @@ CommentSchema.statics.new = function(body, author_id, asset_id, parent_id, statu
|
||||
/**
|
||||
* Finds a comment by the id.
|
||||
* @param {String} id identifier of comment (uuid)
|
||||
*/
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.findById = function(id) {
|
||||
return Comment.findOne({'id': id});
|
||||
};
|
||||
@@ -59,7 +46,8 @@ CommentSchema.statics.findById = function(id) {
|
||||
/**
|
||||
* Finds ALL the comments by the asset_id.
|
||||
* @param {String} asset_id identifier of the asset which owns this comment (uuid)
|
||||
*/
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.findByAssetId = function(asset_id) {
|
||||
return Comment.find({asset_id});
|
||||
};
|
||||
@@ -68,7 +56,8 @@ CommentSchema.statics.findByAssetId = function(asset_id) {
|
||||
* Finds the accepted comments by the asset_id.
|
||||
* get the comments that are accepted.
|
||||
* @param {String} asset_id identifier of the asset which owns the comments (uuid)
|
||||
*/
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.findAcceptedByAssetId = function(asset_id) {
|
||||
return Comment.find({asset_id: asset_id, status:'accepted'});
|
||||
};
|
||||
@@ -76,7 +65,8 @@ CommentSchema.statics.findAcceptedByAssetId = function(asset_id) {
|
||||
/**
|
||||
* Finds the new and accepted comments by the asset_id.
|
||||
* @param {String} asset_id identifier of the asset which owns the comments (uuid)
|
||||
*/
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.findAcceptedAndNewByAssetId = function(asset_id) {
|
||||
return Comment.find({asset_id: asset_id, status: {'$in': ['accepted', '']}});
|
||||
};
|
||||
@@ -84,7 +74,8 @@ CommentSchema.statics.findAcceptedAndNewByAssetId = function(asset_id) {
|
||||
/**
|
||||
* Find comments by an action that was performed on them.
|
||||
* @param {String} action_type the type of action that was performed on the comment
|
||||
*/
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.findByActionType = function(action_type) {
|
||||
return Action
|
||||
.findCommentsIdByActionType(action_type, 'comment')
|
||||
@@ -99,50 +90,54 @@ CommentSchema.statics.findByActionType = function(action_type) {
|
||||
* Find not moderated comments by an action that was performed on them.
|
||||
* @param {String} action_type the type of action that was performed on the comment
|
||||
* @param {String} status the status of the comment to search for
|
||||
*/
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.findByStatusByActionType = function(status, action_type) {
|
||||
return Action
|
||||
.findCommentsIdByActionType(action_type, 'comment')
|
||||
.then((actions) => {
|
||||
|
||||
return Comment.find({
|
||||
'status': status,
|
||||
'id': {
|
||||
'$in': actions.map(a => {
|
||||
return a.item_id;
|
||||
})
|
||||
status: status,
|
||||
id: {
|
||||
$in: actions.map(a => a.item_id)
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Find comments by their status.
|
||||
* @param {String} status the status of the comment to search for
|
||||
*/
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.findByStatus = function(status) {
|
||||
return Comment.find({'status': status});
|
||||
return Comment.find({
|
||||
status: status === 'new' ? '' : status
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Find comments that need to be moderated (aka moderation queue).
|
||||
* @param {String} moderationValue pre or post moderation setting. If it is undefined then look at the settings.
|
||||
*/
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.moderationQueue = function(moderation) {
|
||||
switch(moderation){
|
||||
|
||||
// Pre-moderation: New comments are shown in the moderator queues immediately.
|
||||
case 'pre':
|
||||
return Comment.findByStatus('').then((comments) => {
|
||||
return comments;
|
||||
});
|
||||
|
||||
// Post-moderation: New comments do not appear in moderation queues unless they are flagged by other users.
|
||||
case 'post':
|
||||
return Comment.findByStatusByActionType('', 'flag').then((comments) => {
|
||||
return comments;
|
||||
});
|
||||
|
||||
default:
|
||||
throw new Error('Moderation setting not found.');
|
||||
return Promise.reject(Error('Moderation setting not found.'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -154,16 +149,18 @@ CommentSchema.statics.moderationQueue = function(moderation) {
|
||||
* Change the status of a comment.
|
||||
* @param {String} id identifier of the comment (uuid)
|
||||
* @param {String} status the new status of the comment
|
||||
*/
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.changeStatus = function(id, status) {
|
||||
return Comment.findOneAndUpdate({'id': id}, {$set: {'status': status}}, {upsert: false, new: true});
|
||||
return Comment.findOneAndUpdate({'id': id}, {$set: {'status': status}});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an action to the comment.
|
||||
* @param {String} id identifier of the comment (uuid)
|
||||
* @param {String} action the new action to the comment
|
||||
*/
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.addAction = function(id, user_id, action_type) {
|
||||
// check that the comment exist
|
||||
let action = new Action({
|
||||
@@ -183,7 +180,8 @@ CommentSchema.statics.addAction = function(id, user_id, action_type) {
|
||||
* Change the status of a comment.
|
||||
* @param {String} id identifier of the comment (uuid)
|
||||
* @param {String} status the new status of the comment
|
||||
*/
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.removeById = function(id) {
|
||||
return Comment.remove({'id': id});
|
||||
};
|
||||
@@ -193,7 +191,8 @@ CommentSchema.statics.removeById = function(id) {
|
||||
* @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
|
||||
*/
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.removeAction = function(item_id, user_id, action_type) {
|
||||
return Action.remove({
|
||||
action_type,
|
||||
@@ -203,6 +202,15 @@ CommentSchema.statics.removeAction = function(item_id, user_id, action_type) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all the comments in the collection.
|
||||
* @return {Promise}
|
||||
*/
|
||||
CommentSchema.statics.all = () => {
|
||||
return Comment.find();
|
||||
};
|
||||
|
||||
// Comment model.
|
||||
const Comment = mongoose.model('Comment', CommentSchema);
|
||||
|
||||
module.exports = Comment;
|
||||
|
||||
+3
-2
@@ -2,7 +2,7 @@ const mongoose = require('../mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
/**
|
||||
* this Schema manages application settings that get used on front- and backend
|
||||
* 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
|
||||
@@ -12,7 +12,8 @@ const SettingSchema = new Schema({
|
||||
id: {type: String, default: '1'},
|
||||
moderation: {type: String, enum: ['pre', 'post'], default: 'pre'},
|
||||
infoBoxEnable: {type: Boolean, default: false},
|
||||
infoBoxContent: {type: String, default: ''}
|
||||
infoBoxContent: {type: String, default: ''},
|
||||
wordlist: [String]
|
||||
}, {
|
||||
timestamps: {
|
||||
createdAt: 'created_at',
|
||||
|
||||
+11
-1
@@ -402,7 +402,7 @@ UserService.findById = (id) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds users in an array of idd.
|
||||
* Finds users in an array of ids.
|
||||
* @param {Array} ids array of user identifiers (uuid)
|
||||
*/
|
||||
UserService.findByIdArray = (ids) => {
|
||||
@@ -411,6 +411,16 @@ UserService.findByIdArray = (ids) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds public user information by an array of ids.
|
||||
* @param {Array} ids array of user identifiers (uuid)
|
||||
*/
|
||||
UserService.findPublicByIdArray = (ids) => {
|
||||
return UserModel.find({
|
||||
'id': {$in: ids}
|
||||
}, 'id displayName');
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a JWT from a user email. Only works for local accounts.
|
||||
* @param {String} email of the local user
|
||||
|
||||
+4
-14
@@ -18,13 +18,8 @@
|
||||
"config": {
|
||||
"pre-git": {
|
||||
"commit-msg": [],
|
||||
"pre-commit": [
|
||||
"npm run lint",
|
||||
"npm test"
|
||||
],
|
||||
"pre-push": [
|
||||
"npm test"
|
||||
],
|
||||
"pre-commit": ["npm run lint", "npm test"],
|
||||
"pre-push": ["npm test"],
|
||||
"post-commit": [],
|
||||
"post-merge": []
|
||||
}
|
||||
@@ -33,12 +28,7 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/coralproject/talk.git"
|
||||
},
|
||||
"keywords": [
|
||||
"talk",
|
||||
"coral",
|
||||
"coralproject",
|
||||
"ask"
|
||||
],
|
||||
"keywords": ["talk", "coral", "coralproject", "ask"],
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
@@ -58,13 +48,13 @@
|
||||
"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",
|
||||
"natural": "^0.4.0",
|
||||
"prompt": "^1.0.0",
|
||||
"react-linkify": "^0.1.3",
|
||||
"redis": "^2.6.3",
|
||||
|
||||
@@ -5,9 +5,12 @@ 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');
|
||||
// this route is expecting there to be a token in the hash
|
||||
// see /views/password-reset-email.ejs
|
||||
router.get('/password-reset', (req, res, next) => {
|
||||
// TODO: store the redirect uri in the token or something fancy
|
||||
// admins and regular users should probably be redirected to different places.
|
||||
res.render('password-reset', {redirectUri: process.env.TALK_ROOT_URL});
|
||||
});
|
||||
|
||||
router.get('*', (req, res) => {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const express = require('express');
|
||||
const Action = require('../../../models/action');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.delete('/:action_id', (req, res, next) => {
|
||||
Action
|
||||
.findOneAndRemove({
|
||||
id: req.params.action_id
|
||||
})
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -8,7 +8,7 @@ const router = express.Router();
|
||||
* This returns the user if they are logged in.
|
||||
*/
|
||||
router.get('/', authorization.needed(), (req, res) => {
|
||||
res.json(req.user);
|
||||
res.json(req.user.toObject());
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
+79
-115
@@ -1,147 +1,111 @@
|
||||
const express = require('express');
|
||||
const Comment = require('../../../models/comment');
|
||||
const wordlist = require('../../../services/wordlist');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
//==============================================================================
|
||||
// Get Routes
|
||||
//==============================================================================
|
||||
|
||||
router.get('/', (req, res, next) => {
|
||||
Comment.find({}).then((comments) => {
|
||||
res.status(200).json(comments);
|
||||
let query;
|
||||
|
||||
if (req.query.status) {
|
||||
query = Comment.findByStatus(req.query.status);
|
||||
} else if (req.query.action_type) {
|
||||
query = Comment.findByActionType(req.query.action_type);
|
||||
} else {
|
||||
query = Comment.all();
|
||||
}
|
||||
|
||||
query.then(comments => {
|
||||
res.json(comments);
|
||||
})
|
||||
.catch(next);
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/', wordlist.filter('body'), (req, res, next) => {
|
||||
|
||||
const {
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
author_id
|
||||
} = req.body;
|
||||
|
||||
Comment
|
||||
.create({
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status: req.wordlist.matched ? 'rejected' : '',
|
||||
author_id
|
||||
})
|
||||
.then((comment) => {
|
||||
|
||||
res.status(201).send(comment);
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:comment_id', (req, res, next) => {
|
||||
Comment
|
||||
.findById(req.params.comment_id)
|
||||
.then(comment => {
|
||||
if (!comment) {
|
||||
res.status(404).end();
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json(comment);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
// Get all the comments that have an action_type over them.
|
||||
router.get('/action/:action_type', (req, res, next) => {
|
||||
Comment
|
||||
.findByActionType(req.params.action_type)
|
||||
.then((comments) => {
|
||||
res.status(200).json(comments);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
// Get all the comments that were rejected.
|
||||
router.get('/status/rejected', (req, res, next) => {
|
||||
Comment.findByStatus('rejected').then(comments => {
|
||||
res.status(200).json(comments);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
// Get all the comments that were accepted.
|
||||
router.get('/status/accepted', (req, res, next) => {
|
||||
Comment.findByStatus('accepted').then((comments) => {
|
||||
res.status(200).json(comments);
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
});
|
||||
});
|
||||
|
||||
// Get all the not moderated comments.
|
||||
router.get('/status/new', (req, res, next) => {
|
||||
Comment.findByStatus('').then((comments) => {
|
||||
res.status(200).json(comments);
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
});
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// Post Routes
|
||||
//==============================================================================
|
||||
|
||||
router.post('/', (req, res, next) => {
|
||||
|
||||
const {body, author_id, asset_id, parent_id, status, username} = req.body;
|
||||
|
||||
Comment
|
||||
.new(body, author_id, asset_id, parent_id, status, username)
|
||||
.then((comment) => {
|
||||
res.status(200).send({'id': comment.id});
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:comment_id', (req, res, next) => {
|
||||
Comment
|
||||
.findById(req.params.comment_id)
|
||||
.then((comment) => {
|
||||
comment.body = req.body.body;
|
||||
comment.author_id = req.body.author_id;
|
||||
comment.asset_id = req.body.asset_id;
|
||||
comment.parent_id = req.body.parent_id;
|
||||
comment.status = req.body.status;
|
||||
return comment.save();
|
||||
})
|
||||
.then((comment) => {
|
||||
res.status(200).send(comment);
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:comment_id/status', (req, res, next) => {
|
||||
|
||||
Comment
|
||||
.changeStatus(req.params.comment_id, req.body.status)
|
||||
.then(comment => res.status(200).send(comment))
|
||||
.catch(error => next(error));
|
||||
|
||||
});
|
||||
|
||||
router.post('/:comment_id/actions', (req, res, next) => {
|
||||
Comment
|
||||
.addAction(req.params.comment_id, req.body.user_id, req.body.action_type)
|
||||
.then((action) => {
|
||||
res.status(200).send(action);
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
});
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// Delete Routes
|
||||
//==============================================================================
|
||||
|
||||
router.delete('/:comment_id', (req, res, next) => {
|
||||
Comment
|
||||
.removeById(req.params.comment_id)
|
||||
.then(() => {
|
||||
res.status(201).send({});
|
||||
res.status(204).end();
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
router.delete('/:comment_id/actions', (req, res, next) => {
|
||||
console.log(req.params);
|
||||
router.put('/:comment_id/status', (req, res, next) => {
|
||||
|
||||
const {
|
||||
status
|
||||
} = req.body;
|
||||
|
||||
Comment
|
||||
.removeAction(req.params.comment_id, req.body.user_id, req.body.action_type)
|
||||
.changeStatus(req.params.comment_id, status)
|
||||
.then(() => {
|
||||
res.status(201).send({});
|
||||
res.status(204).end();
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:comment_id/actions', (req, res, next) => {
|
||||
|
||||
const {
|
||||
user_id,
|
||||
action_type
|
||||
} = req.body;
|
||||
|
||||
Comment
|
||||
.addAction(req.params.comment_id, user_id, action_type)
|
||||
.then((action) => {
|
||||
res.status(201).json(action);
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -9,5 +9,6 @@ router.use('/queue', require('./queue'));
|
||||
router.use('/settings', require('./settings'));
|
||||
router.use('/stream', require('./stream'));
|
||||
router.use('/user', require('./user'));
|
||||
router.use('/actions', require('./actions'));
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const express = require('express');
|
||||
const _ = require('lodash');
|
||||
|
||||
const Comment = require('../../../models/comment');
|
||||
const User = require('../../../models/user');
|
||||
@@ -25,9 +26,9 @@ router.get('/', (req, res, next) => {
|
||||
case 'pre':
|
||||
return Promise.all([Comment.findAcceptedByAssetId(asset.id), asset]);
|
||||
case 'post':
|
||||
return Promise.all([Comment.findAcceptedAndNewByAssetId(asset.id), asset]);
|
||||
return Promise.all([Comment.findAcceptedAndNewByAssetId(asset.id), asset]);
|
||||
default:
|
||||
throw new Error('Moderation setting not found.');
|
||||
return Promise.reject(new Error('Moderation setting not found.'));
|
||||
}
|
||||
})
|
||||
// Get all the users and actions for those comments.
|
||||
@@ -35,8 +36,12 @@ router.get('/', (req, res, next) => {
|
||||
return Promise.all([
|
||||
[asset],
|
||||
comments,
|
||||
User.findByIdArray(comments.map((comment) => comment.author_id)),
|
||||
Action.getActionSummaries(comments.map((comment) => comment.id))
|
||||
User.findByIdArray(_.uniq(comments.map((comment) => comment.author_id))),
|
||||
Action.getActionSummaries(_.uniq([
|
||||
asset.id,
|
||||
...comments.map((comment) => comment.id),
|
||||
...comments.map((comment) => comment.author_id)
|
||||
]))
|
||||
]);
|
||||
})
|
||||
.then(([assets, comments, users, actions]) => {
|
||||
|
||||
@@ -79,6 +79,10 @@ router.post('/', (req, res, next) => {
|
||||
router.post('/update-password', (req, res, next) => {
|
||||
const {token, password} = req.body;
|
||||
|
||||
if (!password || password.length < 8) {
|
||||
return res.status(400).send('Password must be at least 8 characters');
|
||||
}
|
||||
|
||||
User.verifyPasswordResetToken(token)
|
||||
.then(user => {
|
||||
return User.changePassword(user.id, password);
|
||||
@@ -100,7 +104,7 @@ router.post('/request-password-reset', (req, res, next) => {
|
||||
const {email} = req.body;
|
||||
|
||||
if (!email) {
|
||||
return next();
|
||||
return next('you must submit an email when requesting a password.');
|
||||
}
|
||||
|
||||
User
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
const debug = require('debug')('talk:services:wordlist');
|
||||
const _ = require('lodash');
|
||||
const natural = require('natural');
|
||||
const tokenizer = new natural.WordTokenizer();
|
||||
const Setting = require('../models/setting');
|
||||
|
||||
/**
|
||||
* The root wordlist object.
|
||||
* @type {Object}
|
||||
*/
|
||||
const wordlist = {
|
||||
list: [],
|
||||
enabled: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads wordlists in from the naughty-words package based on languages
|
||||
* selected.
|
||||
* @param {Array} languages language codes to add to the wordlist
|
||||
*/
|
||||
wordlist.init = () => {
|
||||
return Setting
|
||||
.getSettings()
|
||||
.then((settings) => {
|
||||
|
||||
// Insert the settings wordlist.
|
||||
wordlist.insert(settings.wordlist);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Inserts the wordlist data and enables the wordlist.
|
||||
* @param {Array} list list of words to be added to the wordlist
|
||||
*/
|
||||
wordlist.insert = (list) => {
|
||||
|
||||
// Add the words to this array, but also lowercase the words so that an
|
||||
// easy comparison can take place.
|
||||
wordlist.list = _.uniq(wordlist.list.concat(list.map((word) => {
|
||||
return tokenizer.tokenize(word.toLowerCase());
|
||||
})));
|
||||
|
||||
debug(`Added ${list.length} words to the wordlist, now the wordlist is ${wordlist.list.length} entries long.`);
|
||||
|
||||
// Enable the wordlist.
|
||||
wordlist.enabled = true;
|
||||
|
||||
return Promise.resolve(wordlist);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests the phrase to see if it contains any of the defined blockwords.
|
||||
* @param {String} phrase value to check for blockwords.
|
||||
* @return {Boolean} true if a blockword is found, false otherwise.
|
||||
*/
|
||||
wordlist.match = (phrase) => {
|
||||
|
||||
// Lowercase the word to ensure that we don't miss a match due to
|
||||
// capitalization.
|
||||
let lowerPhraseWords = tokenizer.tokenize(phrase.toLowerCase());
|
||||
|
||||
// This will return true in the event that at least one blockword is found
|
||||
// in the phrase.
|
||||
return wordlist.list.some((blockphrase) => {
|
||||
|
||||
// First, let's see if we can find the first word in the blockphrase in the
|
||||
// source phrase.
|
||||
let idx = lowerPhraseWords.indexOf(blockphrase[0]);
|
||||
|
||||
if (idx === -1) {
|
||||
|
||||
// The first blockword in the blockphrase did not match the source phrase
|
||||
// anywhere.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Here we'll quick respond with true in the event that the blockphrase was
|
||||
// just a single word.
|
||||
if (blockphrase.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We found the first word in the source phrase! Lets ensure it matches the
|
||||
// rest of the blockphrase...
|
||||
|
||||
// Check to see if it even has the length to support this word!
|
||||
if (lowerPhraseWords.length < idx + blockphrase.length - 1) {
|
||||
|
||||
// We couldn't possibly have the entire phrase here because we don't have
|
||||
// enough entries!
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 1; i < blockphrase.length; i++) {
|
||||
|
||||
// Check to see if the next word also matches!
|
||||
if (lowerPhraseWords[idx + i] !== blockphrase[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We've walked over all the words of the blockphrase, and haven't had a
|
||||
// mismatch... It does contain the whole word!
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
// ErrContainsProfanity is returned in the event that the middleware detects
|
||||
// profanity/wordlisted words in the payload.
|
||||
const ErrContainsProfanity = new Error('contains profanity');
|
||||
ErrContainsProfanity.status = 400;
|
||||
|
||||
/**
|
||||
* Connect middleware for scanning request bodies for wordlisted words and
|
||||
* attaching a ErrContainsProfanity to the req.wordlisted parameter, otherwise
|
||||
* it will just set that parameter to false.
|
||||
* @param {Array} fields selectors for the body to extract the fields to be
|
||||
* tested
|
||||
* @return {Function} the Connect middleware
|
||||
*/
|
||||
wordlist.filter = (...fields) => (req, res, next) => {
|
||||
|
||||
// Start with the sensible default that the content does not contain
|
||||
// profanity.
|
||||
req.wordlist = {
|
||||
matched: false
|
||||
};
|
||||
|
||||
// If the wordlist isn't enabled, then don't actually perform checking and
|
||||
// forward the request!
|
||||
if (!wordlist.enabled) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Loop over all the fields from the body that we want to check.
|
||||
const containsProfanity = fields.some((field) => {
|
||||
let phrase = _.get(req.body, field, false);
|
||||
|
||||
// If the field doesn't exist in the body, then it can't be profane!
|
||||
if (!phrase) {
|
||||
|
||||
// Return that there wasn't a profane word here.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the field contains a profane word.
|
||||
if (wordlist.match(phrase)) {
|
||||
debug(`the field "${field}" contained a phrase "${phrase}" which contained a wordlisted word/phrase`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// The body could contain some profanity, address that here.
|
||||
if (containsProfanity) {
|
||||
req.wordlist.matched = ErrContainsProfanity;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = wordlist;
|
||||
module.exports.ErrContainsProfanity = ErrContainsProfanity;
|
||||
+262
-158
@@ -7,221 +7,320 @@ host: talk-stg.coralproject.net
|
||||
schemes:
|
||||
- https
|
||||
basePath: /api/v1
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
|
||||
paths:
|
||||
/comments:
|
||||
# get:
|
||||
# tags:
|
||||
# - Comments
|
||||
# produces:
|
||||
# - application/json
|
||||
# summary: Comment Types
|
||||
# description: |
|
||||
# This endpoint retrieves comments
|
||||
# parameters:
|
||||
# - name: id
|
||||
# in: query
|
||||
# description: Comment by id
|
||||
# required: false
|
||||
# type: string
|
||||
# responses:
|
||||
# 200:
|
||||
# description: An array of comments
|
||||
# schema:
|
||||
# type: array
|
||||
# items:
|
||||
# $ref: '#/definitions/Comment'
|
||||
get:
|
||||
tags:
|
||||
- Comments
|
||||
parameters:
|
||||
- name: status
|
||||
in: query
|
||||
description: Performs a search based on the comment's status.
|
||||
type: string
|
||||
enum:
|
||||
- flag
|
||||
- name: action_type
|
||||
in: query
|
||||
description: Performs a search based on the actions that have been added to it.
|
||||
type: string
|
||||
enum:
|
||||
- rejected
|
||||
- accepted
|
||||
- new
|
||||
responses:
|
||||
200:
|
||||
description: Comments matching the query.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
- $ref: '#/definitions/Comment'
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
post:
|
||||
description: Add a new comment
|
||||
tags:
|
||||
- Comments
|
||||
parameters:
|
||||
- name: body
|
||||
in: body
|
||||
description: Body
|
||||
required: true
|
||||
description: The comment to create.
|
||||
schema:
|
||||
$ref: '#/definitions/Comment'
|
||||
responses:
|
||||
201:
|
||||
description: "OK: Comment Added"
|
||||
description: The comment that was created.
|
||||
schema:
|
||||
$ref: '#/definitions/Comment'
|
||||
$ref: '#/definitions/Comment'
|
||||
500:
|
||||
description: "Error"
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
/comments/{comment_id}:
|
||||
get:
|
||||
tags:
|
||||
- Comments
|
||||
parameters:
|
||||
- name: comment_id
|
||||
in: path
|
||||
description: The id of the comment to retrieve.
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: The comment was found.
|
||||
schema:
|
||||
$ref: '#/definitions/Comment'
|
||||
404:
|
||||
description: The comment was not found.
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
delete:
|
||||
tags:
|
||||
- Comments
|
||||
parameters:
|
||||
- name: comment_id
|
||||
in: path
|
||||
description: The id of the comment to delete.
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
204:
|
||||
description: The comment was deleted.
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
/comments/{comment_id}/status:
|
||||
put:
|
||||
tags:
|
||||
- Comments
|
||||
- Moderation
|
||||
parameters:
|
||||
- name: comment_id
|
||||
in: path
|
||||
description: The id of the comment to retrieve.
|
||||
type: string
|
||||
required: true
|
||||
- name: body
|
||||
in: body
|
||||
description: The status to update to.
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: The status to update to.
|
||||
responses:
|
||||
204:
|
||||
description: The comment status was updated.
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
/comments/{comment_id}/actions:
|
||||
post:
|
||||
tags:
|
||||
- Comments
|
||||
description: Add a action
|
||||
- Actions
|
||||
parameters:
|
||||
- name: comment_id
|
||||
in: path
|
||||
description: Comment ID
|
||||
required: true
|
||||
description: The id of the comment to retrieve.
|
||||
type: string
|
||||
required: true
|
||||
- name: body
|
||||
in: body
|
||||
description: comment
|
||||
description: The action to add.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/Action'
|
||||
type: object
|
||||
properties:
|
||||
action_type:
|
||||
type: string
|
||||
description: The action to add
|
||||
responses:
|
||||
201:
|
||||
description: Action Added
|
||||
description: The action created.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Comment'
|
||||
/comments/{comment_id}/status:
|
||||
post:
|
||||
$ref: '#/definitions/Action'
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
/actions/{action_id}:
|
||||
delete:
|
||||
tags:
|
||||
- Comments
|
||||
description: Add a new status
|
||||
- Actions
|
||||
parameters:
|
||||
- name: comment_id
|
||||
- name: action_id
|
||||
in: path
|
||||
description: Comment ID
|
||||
required: true
|
||||
description: The id of the action to delete.
|
||||
type: string
|
||||
- name: body
|
||||
in: body
|
||||
description: comment
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/ModerationAction'
|
||||
responses:
|
||||
204:
|
||||
description: ModerationAction Added
|
||||
/queue:
|
||||
description: The action was deleted.
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
/auth:
|
||||
get:
|
||||
tags:
|
||||
- Queue
|
||||
description: Queue
|
||||
parameters:
|
||||
- name: type
|
||||
in: query
|
||||
description:
|
||||
"pending: no status | flagged: flagged action + no status | rejected: rejected status"
|
||||
required: true
|
||||
type: string
|
||||
enum:
|
||||
- pending
|
||||
- flagged
|
||||
- rejected
|
||||
- name: limit
|
||||
in: query
|
||||
description: Queue limit
|
||||
required: false
|
||||
type: integer
|
||||
- name: skip
|
||||
in: query
|
||||
description: Skip
|
||||
required: false
|
||||
type: integer
|
||||
- Auth
|
||||
description: Retrieves the current authentication credentials.
|
||||
responses:
|
||||
200:
|
||||
description: ModerationAction Added
|
||||
description: The current user.
|
||||
schema:
|
||||
$ref: '#/definitions/User'
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
delete:
|
||||
tags:
|
||||
- Auth
|
||||
description: Logs out the current authenticated user.
|
||||
responses:
|
||||
204:
|
||||
description: The current user has been logged out.
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
/auth/local:
|
||||
post:
|
||||
tags:
|
||||
- Auth
|
||||
parameters:
|
||||
- name: body
|
||||
in: body
|
||||
required: true
|
||||
description: The login credentials.
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
description: The email address of the current user.
|
||||
password:
|
||||
type: string
|
||||
description: The password of the current user.
|
||||
responses:
|
||||
200:
|
||||
description: The user has authenticated sucesfully.
|
||||
schema:
|
||||
$ref: '#/definitions/User'
|
||||
401:
|
||||
description: The authentication error.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
/auth/facebook:
|
||||
get:
|
||||
tags:
|
||||
- Auth
|
||||
responses:
|
||||
302:
|
||||
description: Redirects the user to perform external facebook authentication.
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
/queue/comments/pending:
|
||||
get:
|
||||
tags:
|
||||
- Comments
|
||||
- Moderation
|
||||
responses:
|
||||
200:
|
||||
description: The comments that are not moderated.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/ModerationAction'
|
||||
- $ref: '#/definitions/Comment'
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
/stream:
|
||||
get:
|
||||
tags:
|
||||
- Stream
|
||||
description: Stream
|
||||
- Actions
|
||||
- Assets
|
||||
- Comments
|
||||
- Users
|
||||
parameters:
|
||||
- name: asset_id
|
||||
- name: asset_url
|
||||
in: query
|
||||
description: Description
|
||||
required: true
|
||||
description: The asset url to get the comment stream from.
|
||||
type: string
|
||||
format: url
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
description: The comment stream.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Item'
|
||||
type: object
|
||||
properties:
|
||||
assets:
|
||||
type: array
|
||||
items:
|
||||
- $ref: '#/definitions/Asset'
|
||||
comments:
|
||||
type: array
|
||||
items:
|
||||
- $ref: '#/definitions/Comment'
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
- $ref: '#/definitions/User'
|
||||
actions:
|
||||
type: array
|
||||
items:
|
||||
- $ref: '#/definitions/Actions'
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
/settings:
|
||||
get:
|
||||
tags:
|
||||
- Settings
|
||||
description: Settings
|
||||
responses:
|
||||
200:
|
||||
description: Get Setting
|
||||
description: The settings.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Setting'
|
||||
$ref: '#/definitions/Settings'
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
put:
|
||||
tags:
|
||||
- Settings
|
||||
description: Settings
|
||||
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:
|
||||
- Asset
|
||||
description: Get an asset by id.
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
put:
|
||||
tags:
|
||||
- Asset
|
||||
description: Upsert an asset.
|
||||
responses:
|
||||
204:
|
||||
description: OK
|
||||
/asset?url={url}:
|
||||
get:
|
||||
tags:
|
||||
- Asset
|
||||
parameters:
|
||||
- name: url
|
||||
in: query
|
||||
description: The url of the asset.
|
||||
required: true
|
||||
description: Get an asset by its url.
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
|
||||
description: The settings were updated.
|
||||
500:
|
||||
description: An error occured.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
definitions:
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: The error that occured.
|
||||
Item:
|
||||
type: object
|
||||
ModerationAction:
|
||||
@@ -243,9 +342,9 @@ definitions:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Display name of comment
|
||||
user_id:
|
||||
author_id:
|
||||
type: string
|
||||
description: Display name of comment
|
||||
description: User who posted the comment
|
||||
parent_id:
|
||||
type: string
|
||||
description: Display name of comment
|
||||
@@ -314,5 +413,10 @@ definitions:
|
||||
type: string
|
||||
description: An array of the authors for this asset.
|
||||
publication_date:
|
||||
type: date
|
||||
desctipion: When this asset was published.
|
||||
type: string
|
||||
format: datetime
|
||||
description: When this asset was published.
|
||||
User:
|
||||
type: object
|
||||
Settings:
|
||||
type: object
|
||||
|
||||
@@ -29,21 +29,23 @@ describe('itemActions', () => {
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: 'like',
|
||||
id: '123',
|
||||
action_type: 'like',
|
||||
item_id: '123',
|
||||
count: 1,
|
||||
id: 'like_123',
|
||||
current_user: false
|
||||
},
|
||||
{
|
||||
type: 'flag',
|
||||
id: '456',
|
||||
action_type: 'flag',
|
||||
item_id: '456',
|
||||
count: 5,
|
||||
id: 'flag_456',
|
||||
current_user: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
it('should get an stream from an asset_id and send the appropriate dispatches', () => {
|
||||
it('should get an stream from an asset_url and send the appropriate dispatches', () => {
|
||||
fetchMock.get('*', JSON.stringify(response));
|
||||
return actions.getStream(assetUrl)(store.dispatch)
|
||||
.then((res) => {
|
||||
@@ -173,7 +175,7 @@ describe('itemActions', () => {
|
||||
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(fetchMock.calls().matched[0][0]).to.equal('/api/v1/actions/abc');
|
||||
expect(response).to.deep.equal({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,10 +10,6 @@ describe('Action: models', () => {
|
||||
action_type: 'flag',
|
||||
item_id: '123',
|
||||
item_type: 'comments'
|
||||
}, {
|
||||
action_type: 'like',
|
||||
item_id: '789',
|
||||
item_type: 'comments'
|
||||
}, {
|
||||
action_type: 'flag',
|
||||
item_id: '456',
|
||||
@@ -22,6 +18,10 @@ describe('Action: models', () => {
|
||||
action_type: 'flag',
|
||||
item_id: '123',
|
||||
item_type: 'comments'
|
||||
}, {
|
||||
action_type: 'like',
|
||||
item_id: '123',
|
||||
item_type: 'comments'
|
||||
}]).then((actions) => {
|
||||
mockActions = actions;
|
||||
});
|
||||
@@ -39,7 +39,7 @@ describe('Action: models', () => {
|
||||
describe('#findByItemIdArray()', () => {
|
||||
it('should find an array of actions from an array of item_ids', () => {
|
||||
return Action.findByItemIdArray(['123', '456']).then((result) => {
|
||||
expect(result).to.have.length(3);
|
||||
expect(result).to.have.length(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -48,16 +48,17 @@ describe('Action: models', () => {
|
||||
it('should return properly formatted summaries from an array of item_ids', () => {
|
||||
return Action.getActionSummaries(['123', '789']).then((result) => {
|
||||
expect(result).to.have.length(2);
|
||||
|
||||
const sorted = result.sort((a, b) => a.count - b.count);
|
||||
delete sorted[0].id;
|
||||
delete sorted[1].id;
|
||||
|
||||
expect(sorted[0]).to.deep.equal({
|
||||
action_type: 'like',
|
||||
count: 1,
|
||||
item_id: '789',
|
||||
item_id: '123',
|
||||
item_type: 'comments',
|
||||
current_user: false
|
||||
});
|
||||
|
||||
expect(sorted[1]).to.deep.equal({
|
||||
action_type: 'flag',
|
||||
count: 2,
|
||||
|
||||
@@ -43,6 +43,22 @@ describe('User: models', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findPublicByIdArray()', () => {
|
||||
it('should find an array of users from an array of ids', () => {
|
||||
const ids = mockUsers.map((user) => user.id);
|
||||
return User.findPublicByIdArray(ids).then((result) => {
|
||||
expect(result).to.have.length(3);
|
||||
const sorted = result.sort((a, b) => {
|
||||
if(a.displayName < b.displayName) {return -1;}
|
||||
if(a.displayName > b.displayName) {return 1;}
|
||||
return 0;
|
||||
});
|
||||
expect(sorted[0]).to.have.property('displayName')
|
||||
.and.to.equal('Marvel');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findLocalUser', () => {
|
||||
|
||||
it('should find a user when we give the right credentials', () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ const expect = chai.expect;
|
||||
chai.should();
|
||||
chai.use(require('chai-http'));
|
||||
|
||||
const wordlist = require('../../../../services/wordlist');
|
||||
const Comment = require('../../../../models/comment');
|
||||
const Action = require('../../../../models/action');
|
||||
const User = require('../../../../models/user');
|
||||
@@ -64,13 +65,13 @@ describe('Get /comments', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return all the comments', function(done){
|
||||
chai.request(app)
|
||||
it('should return all the comments', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments')
|
||||
.end(function(err, res){
|
||||
expect(err).to.be.null;
|
||||
.then((res) => {
|
||||
|
||||
expect(res).to.have.status(200);
|
||||
done();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -122,48 +123,42 @@ describe('Get comments by status and action', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return all the rejected comments', function(done){
|
||||
chai.request(app)
|
||||
.get('/api/v1/comments/status/rejected')
|
||||
.end(function(err, res){
|
||||
expect(err).to.be.null;
|
||||
it('should return all the rejected comments', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments?status=rejected')
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body[0]).to.have.property('id', 'abc');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return all the approved comments', function(done){
|
||||
chai.request(app)
|
||||
.get('/api/v1/comments/status/accepted')
|
||||
.end(function(err, res){
|
||||
expect(err).to.be.null;
|
||||
it('should return all the approved comments', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments?status=accepted')
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body[0]).to.have.property('id', 'hij');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return all the new comments', function(done){
|
||||
chai.request(app)
|
||||
.get('/api/v1/comments/status/new')
|
||||
.end(function(err, res){
|
||||
expect(err).to.be.null;
|
||||
it('should return all the new comments', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments?status=new')
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body[0]).to.have.property('id', 'def');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return all the flagged comments', function(done){
|
||||
chai.request(app)
|
||||
.get('/api/v1/comments/action/flag')
|
||||
.end(function(err, res){
|
||||
it('should return all the flagged comments', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments?action_type=flag')
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(err).to.be.null;
|
||||
|
||||
expect(res.body.length).to.equal(1);
|
||||
expect(res.body[0]).to.have.property('id', 'abc');
|
||||
done();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -190,18 +185,31 @@ describe('Post /comments', () => {
|
||||
beforeEach(() => {
|
||||
return Promise.all([
|
||||
User.createLocalUsers(users),
|
||||
Action.create(actions)
|
||||
Action.create(actions),
|
||||
wordlist.insert([
|
||||
'bad words'
|
||||
])
|
||||
]);
|
||||
});
|
||||
|
||||
it('it should create a comment', function(done) {
|
||||
chai.request(app)
|
||||
it('should create a comment', () => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments')
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': '1', 'parent_id': ''})
|
||||
.end(function(err, res){
|
||||
expect(res).to.have.status(200);
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a comment with a rejected status if it contains a bad word', () => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments')
|
||||
.send({'body': 'bad words are the baddest', 'author_id': '123', 'asset_id': '1', 'parent_id': ''})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
expect(res.body).to.have.property('status', 'rejected');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -251,72 +259,14 @@ describe('Get /:comment_id', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the right comment for the comment_id', function(done){
|
||||
chai.request(app)
|
||||
it('should return the right comment for the comment_id', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/comments/abc')
|
||||
.end(function(err, res){
|
||||
expect(err).to.be.null;
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res).to.have.property('body');
|
||||
expect(res.body).to.have.property('body', 'comment 10');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Put /:comment_id', () => {
|
||||
|
||||
const comments = [{
|
||||
id: 'abc',
|
||||
body: 'comment 10',
|
||||
asset_id: 'asset',
|
||||
author_id: '123'
|
||||
}, {
|
||||
id: 'def',
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456'
|
||||
}, {
|
||||
id: 'hij',
|
||||
body: 'comment 30',
|
||||
asset_id: '456'
|
||||
}];
|
||||
|
||||
const users = [{
|
||||
displayName: 'Ana',
|
||||
email: 'ana@gmail.com',
|
||||
password: '123'
|
||||
}, {
|
||||
displayName: 'Maria',
|
||||
email: 'maria@gmail.com',
|
||||
password: '123'
|
||||
}];
|
||||
|
||||
const actions = [{
|
||||
action_type: 'flag',
|
||||
item_id: 'abc'
|
||||
}, {
|
||||
action_type: 'like',
|
||||
item_id: 'hij'
|
||||
}];
|
||||
|
||||
beforeEach(() => {
|
||||
return Promise.all([
|
||||
Comment.create(comments),
|
||||
User.createLocalUsers(users),
|
||||
Action.create(actions)
|
||||
]);
|
||||
});
|
||||
|
||||
it('it should update comment', function(done) {
|
||||
chai.request(app)
|
||||
.post('/api/v1/comments/abc')
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': '1', 'parent_id': ''})
|
||||
.end(function(err, res){
|
||||
expect(err).to.be.null;
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body).to.have.property('body', 'Something body.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -369,7 +319,7 @@ describe('Remove /:comment_id', () => {
|
||||
return chai.request(app)
|
||||
.delete('/api/v1/comments/abc')
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res).to.have.status(204);
|
||||
|
||||
return Comment.findById('abc');
|
||||
})
|
||||
@@ -384,7 +334,7 @@ process.on('unhandledRejection', (reason) => {
|
||||
console.error(reason);
|
||||
});
|
||||
|
||||
describe('Post /:comment_id/status', () => {
|
||||
describe('Put /:comment_id/status', () => {
|
||||
|
||||
const comments = [{
|
||||
id: 'abc',
|
||||
@@ -433,12 +383,11 @@ describe('Post /:comment_id/status', () => {
|
||||
|
||||
it('it should update status', function() {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments/abc/status')
|
||||
.put('/api/v1/comments/abc/status')
|
||||
.send({status: 'accepted'})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res).to.have.body;
|
||||
expect(res.body).to.have.property('status', 'accepted');
|
||||
expect(res).to.have.status(204);
|
||||
expect(res.body).to.be.empty;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -495,7 +444,7 @@ describe('Post /:comment_id/actions', () => {
|
||||
.post('/api/v1/comments/abc/actions')
|
||||
.send({'user_id': '456', 'action_type': 'flag'})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res).to.have.status(201);
|
||||
expect(res).to.have.body;
|
||||
expect(res.body).to.have.property('item_type', 'comment');
|
||||
expect(res.body).to.have.property('action_type', 'flag');
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
const expect = require('chai').expect;
|
||||
|
||||
const wordlist = require('../../services/wordlist');
|
||||
|
||||
describe('wordlist: services', () => {
|
||||
|
||||
before(() => wordlist.insert([
|
||||
'BAD',
|
||||
'bad',
|
||||
'how to murder',
|
||||
'how to kill'
|
||||
]));
|
||||
|
||||
beforeEach(() => {
|
||||
expect(wordlist.list).to.not.be.empty;
|
||||
expect(wordlist.enabled).to.be.true;
|
||||
});
|
||||
|
||||
describe('#init', () => {
|
||||
|
||||
it('has entries', () => {
|
||||
expect(wordlist.list).to.not.be.empty;
|
||||
expect(wordlist.enabled).to.be.true;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#match', () => {
|
||||
|
||||
it('does match on a bad word', () => {
|
||||
[
|
||||
'how to kill',
|
||||
'what is bad',
|
||||
'bad',
|
||||
'BAD.',
|
||||
'how to murder',
|
||||
'How To mUrDer'
|
||||
].forEach((word) => {
|
||||
expect(wordlist.match(word)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('does not match on a good word', () => {
|
||||
[
|
||||
'how to',
|
||||
'kill',
|
||||
'bads',
|
||||
'how to be a great person?',
|
||||
'how to not kill?'
|
||||
].forEach((word) => {
|
||||
expect(wordlist.match(word)).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#filter', () => {
|
||||
|
||||
it('matches on bodies containing bad words', (done) => {
|
||||
|
||||
let req = {
|
||||
body: {
|
||||
content: 'how to kill?'
|
||||
}
|
||||
};
|
||||
|
||||
wordlist.filter('content')(req, {}, (err) => {
|
||||
expect(err).to.be.undefined;
|
||||
expect(req).to.have.property('wordlist');
|
||||
expect(req.wordlist).to.have.property('matched');
|
||||
expect(req.wordlist.matched).to.be.equal(wordlist.ErrContainsProfanity);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('does not match on bodies not containing bad words', (done) => {
|
||||
|
||||
let req = {
|
||||
body: {
|
||||
content: 'how to be a great person?'
|
||||
}
|
||||
};
|
||||
|
||||
wordlist.filter('content')(req, {}, (err) => {
|
||||
expect(err).to.be.undefined;
|
||||
expect(req).to.have.property('wordlist');
|
||||
expect(req.wordlist).to.have.property('matched');
|
||||
expect(req.wordlist.matched).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('does not match on bodies not containing the bad word field', (done) => {
|
||||
|
||||
let req = {
|
||||
body: {
|
||||
author: 'how to kill?',
|
||||
content: 'how to be a great person?'
|
||||
}
|
||||
};
|
||||
|
||||
wordlist.filter('content')(req, {}, (err) => {
|
||||
expect(err).to.be.undefined;
|
||||
expect(req).to.have.property('wordlist');
|
||||
expect(req.wordlist).to.have.property('matched');
|
||||
expect(req.wordlist.matched).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- extremely naive implementation of a password reset email -->
|
||||
<p>We received a request to reset your password. If you did not request this change, you can ignore this email.<br />
|
||||
If you did, <a href="<%= rootURL %>/admin/password-reset/<%= token %>">please click here to reset password</a>.</p>
|
||||
If you did, <a href="<%= rootURL %>/admin/password-reset#<%= token %>">please click here to reset password</a>.</p>
|
||||
<% if (process.env.NODE_ENV !== 'production') { %>
|
||||
<p style="color: red"><%= token %></p>
|
||||
<% } %>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<title>Password Reset</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
|
||||
<style media="screen">
|
||||
body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#root form {
|
||||
max-width: 300px;
|
||||
border: 1px solid lightgrey;
|
||||
box-shadow: 0px 10px 24px 2px rgba(0,0,0,0.2);
|
||||
margin: 50px auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 3px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 4px;
|
||||
margin-top: 3px;
|
||||
border: 1px solid lightgrey;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.submit-password-reset {
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
display: block;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error-console {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
border-radius: 4px;
|
||||
background-color: pink;
|
||||
color: red;
|
||||
border: 1px solid red;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.error-console.active {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<form id="reset-password-form">
|
||||
<legend class="legend">Set new password</legend>
|
||||
<label for="password">
|
||||
New password
|
||||
<input type="password" name="password" placeholder="new password" />
|
||||
<p><small>Password must be at least 8 characters</small></p>
|
||||
</label>
|
||||
|
||||
<label for="confirm-password">
|
||||
Confirm password
|
||||
<input type="password" name="confirm-password" placeholder="confirm password" />
|
||||
</label>
|
||||
<button class="submit-password-reset" type="submit">Apply</button>
|
||||
<div class="error-console">foo</div>
|
||||
</form>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
function showError(message) {
|
||||
$('.error-console').text(message).addClass('active');
|
||||
}
|
||||
|
||||
function handleSubmit (e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
var password = $('[name="password"]').val();
|
||||
var confirm = $('[name="confirm-password"]').val();
|
||||
|
||||
if (password !== confirm || password === '' || password.length < 8) {
|
||||
showError('passwords must match and be 8 characters.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/api/v1/user/update-password',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({password: password, token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = '<%= redirectUri %>';
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$('#reset-password-form').on('submit', handleSubmit);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user