Mod Queue Working

This commit is contained in:
Belen Curcio
2017-02-08 12:36:05 -03:00
102 changed files with 3083 additions and 991 deletions
+7 -9
View File
@@ -1,9 +1,10 @@
# Contributor's Guide
Welcome! We are very excited that you are interested in contributing to Talk.
Welcome! We are very excited that you are interested in contributing to Talk.
This document is a companion to help you approach contributing. If it does not do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)!
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct.html).
## Product Roadmap
@@ -13,7 +14,7 @@ You can view product ideas and our longer term roadmap here https://trello.com/b
## Contribute to the documentation
Clear docs are a prerequisite for a successful open source project. We value non-code and code contributions equally.
Clear docs are a prerequisite for a successful open source project. We value non-code and code contributions equally.
We are looking for _documentarians_ to:
@@ -40,7 +41,7 @@ Talk is designed to integrate into existing environments in a variety of ways:
If you're considering deploying Talk, [please let us know](https://github.com/coralproject/talk/wiki/Contact-Us)! We are quite literally doing this for you and want to help you succeed any way we can.
If you are writing custom integration code in your fork of Talk, please consider keeping it generic and filing a Pull Request to contribute it back to the project! See our [forking and merging guidelines](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging) for more info.
If you are writing custom integration code in your fork of Talk, please consider keeping it generic and filing a Pull Request to contribute it back to the project! See our [forking and merging guidelines](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging) for more info.
## Write some code
@@ -48,9 +49,9 @@ First, [set up a dev environment](https://github.com/coralproject/talk/blob/mast
### Build a New Feature / Plugin
Talk is beginning life as a Commenting Platform, but is architected to support many varieties of community engagement.
Talk is beginning life as a Commenting Platform, but is architected to support many varieties of community engagement.
Please [contact us](https://github.com/coralproject/talk/wiki/Contact-Us) early and often if you'd like to help. We would love to hear your ideas for features and plugins and help you find a way to productively engage the project.
Please [contact us](https://github.com/coralproject/talk/wiki/Contact-Us) early and often if you'd like to help. We would love to hear your ideas for features and plugins and help you find a way to productively engage the project.
To get an idea of where the Coral Team is going, see:
@@ -66,13 +67,10 @@ Examples:
### Work on the Core
There is always more work to be done to make an application more stable, scaleable and secure.
There is always more work to be done to make an application more stable, scaleable and secure.
If you see issues in the code or have ideas on how we may improve Talk, please consider:
* [contributing a fix](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging),
* [filing an issue](https://github.com/coralproject/talk/issues), or
* or otherwise [letting us know](https://github.com/coralproject/talk/wiki/Contact-Us).
+60 -61
View File
@@ -1,95 +1,94 @@
# Installing a dev environment
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct.html).
# Installation
## Requirements
### System
- Any flavor of Linux, OSX or Windows
- Any flavour of Linux, OSX or Windows
- 1GB memory (minimum)
- 5GB storage (minimum)
### Software
## Installation From Source
* [Node](https://nodejs.org/es/download/package-manager) v7 or later
* Mongo v3.2 or later
* Redis v3.2 or later
### Requirements
There are some runtime requirements for running Talk from source:
- [Node](https://nodejs.org/) v7 or later
- [MongoDB](https://www.mongodb.com/) v3.2 or later
- [Redis](https://redis.io/) v3.2 or later
- [Yarn](https://yarnpkg.com/) v0.19.1 or later
_Please be sure to check the versions of these requirements. Insufficient versions of these may lead to unexpected errors!_
## First time setup
### Installing
### Installation
```bash
# Download the tarball containing the repository
curl -L https://github.com/coralproject/talk/tarball/master -o coralproject-talk.tar.gz
Navigate to a directory.
# Untar that file and change to that directory
tar xpf coralproject-talk.tar.gz
mv coralproject-talk-* coralproject-talk
cd coralproject-talk
```
git clone https://github.com/coralproject/talk
cd talk
yarn install
```
# Install package dependancies
yarn
### Environmental Variables
Talk uses environmental variables for configuration. You can learn about them in the [README file](README.md).
## Workflows
### The server
Starting the server:
```
yarn start
```
Browse to `http://localhost:3000` (or your custom port.)
### Building the front end
Our build process will build all front end components registered [here](https://github.com/coralproject/talk/blob/6052cac1d3494f8060325a88bb2ce03c88c2f94c/webpack.config.dev.js#L9-L15).
One time build:
```
# Build static files
yarn build
```
Build, then rebuild when a file is updated (development build):
### Running
```
yarn build-watch
Refer to the `README.md` file for required configuration variables to add to the
environment.
You can start the server after configuring the server using the command:
```bash
yarn start
```
You can see other scripts we've made available by consulting the `package.json`
file under the `scripts` key including:
### Testing
- `yarn test` run unit tests
- `yarn e2e` run end to end tests
- `yarn build-watch` watch for changes to client files and build static assets
- `yarn dev-start` watch for changes to server files and reload the server
Run all tests once:
## Installation From Docker Hub
`
yarn test
`
### Requirements
Run our end to end tests (will install Selenium and nightwatch):
There are some runtime requirements for running Talk for Docker:
`
yarn e2e
`
- [MongoDB](https://www.mongodb.com/) v3.2 or later
- [Redis](https://redis.io/) v3.2 or later
- [Docker](https://www.docker.com/) v1.13.0 or later
- [Docker Compose](https://docs.docker.com/compose/) v1.10.0 or later
_Please ensure all tests are passing before submitting a PR!_
_Please be sure to check the versions of these requirements. Insufficient versions of these may lead to unexpected errors!_
## Troubleshooting
### Installing
```bash
# Create a directory for talk
mkdir coralproject-talk
cd coralproject-talk
##### Can't ping the redis server!
# Download the docker-compose.yml file from the repository
curl -LO https://raw.githubusercontent.com/coralproject/talk/master/docker-compose.yml
```
- Check that Redis Server is running.
- Check that TALK_REDIS_URL is set.
##### Authenticaiton doesn't work!
- Make sure Redis is the correct version.
At this stage, you should refer to the `README.md` file for required
configuration variables to add to the environment key for the `talk` service
listed in the `docker-compose.yml` file.
### Running
```bash
# Start the services using compose
docker-compose up -d
```
+3 -2
View File
@@ -1,6 +1,6 @@
# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
A commenting platform from [The Coral Project](https://coralproject.net).
A commenting platform from [The Coral Project](https://coralproject.net). Talk enters a closed beta in March 2017, but you can download the code for our alpha here. [Read more about Talk here.](https://coralproject.net/products/talk.html)
## Contributing to Talk
@@ -12,7 +12,7 @@ See our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CO
To set up a development environment or build from source, see [INSTALL.md](https://github.com/coralproject/talk/blob/master/INSTALL.md).
To launch a Talk server of your own from your browser without any need to muck about in a terminal or think about engineering concepts, stay tuned. We will launch [our installer](https://github.com/coralproject/talk-install) shortly!!
To launch a Talk server of your own from your browser without any need to muck about in a terminal or think about engineering concepts, stay tuned. We will launch [our installer](https://github.com/coralproject/talk-install) shortly!
### Configuration
@@ -37,6 +37,7 @@ Facebook Login enabled app.
- `TALK_SMTP_PASSWORD` (*required for email*) - password for the SMTP provider you are using.
- `TALK_SMTP_HOST` (*required for email*) - SMTP host url with format `smtp.domain.com`.
- `TALK_SMTP_PORT` (*required for email*) - SMTP port.
- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - Defaults to `FALSE`. When `TRUE`, disables the dynamic setup endpoint.
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
alternative methods of loading configuration during development.
+11 -15
View File
@@ -4,7 +4,7 @@
* Module dependencies.
*/
const util = require('../util');
// const util = require('./util');
const program = require('./commander');
program
@@ -15,18 +15,14 @@ program
.command('users', 'work with the application auth')
.parse(process.argv);
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
return;
}
// The ensures that the child process that is created here is always cleaned up
// properly when the parent process dies.
util.onshutdown([
(signal) => {
if ((program.runningCommand.killed === false) && (program.runningCommand.exitCode === null)) {
program.runningCommand.kill(signal);
}
/**
* When this provess exists, check to see if we have a running command, if we do
* check to see if it is still running. If it is, then kill it with a SIGINT
* signal. This is for the use case where we want to kill the process that is
* labled with the PID written out by the parent process.
*/
process.once('exit', () => {
if ((program.runningCommand.killed === false) && (program.runningCommand.exitCode === null)) {
program.runningCommand.kill('SIGINT');
}
]);
});
+1 -1
View File
@@ -10,7 +10,7 @@ const Table = require('cli-table');
const AssetModel = require('../models/asset');
const mongoose = require('../services/mongoose');
const scraper = require('../services/scraper');
const util = require('../util');
const util = require('./util');
// Register the shutdown criteria.
util.onshutdown([
+1 -1
View File
@@ -7,7 +7,7 @@
const program = require('./commander');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const util = require('../util');
const util = require('./util');
const mongoose = require('../services/mongoose');
const kue = require('../services/kue');
+2 -3
View File
@@ -2,13 +2,12 @@
const app = require('../app');
const program = require('./commander');
const debug = require('debug')('talk:server');
const http = require('http');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const kue = require('../services/kue');
const mongoose = require('../services/mongoose');
const util = require('../util');
const util = require('./util');
/**
* Get port from environment and store in Express.
@@ -79,7 +78,7 @@ function onListening() {
let bind = typeof addr === 'string'
? `pipe ${ addr}`
: `port ${ addr.port}`;
debug(`Listening on ${ bind}`);
console.log(`Listening on ${ bind}`);
}
/**
+162 -54
View File
@@ -9,7 +9,10 @@ const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const SettingModel = require('../models/setting');
const SettingsService = require('../services/settings');
const util = require('../util');
const SetupService = require('../services/setup');
const UsersService = require('../services/users');
const util = require('./util');
const errors = require('../errors');
// Register the shutdown criteria.
util.onshutdown([
@@ -29,60 +32,165 @@ program
// Setup the application
//==============================================================================
SettingsService
.init()
.then((settings) => {
if (program.defaults) {
return settings.save();
}
const performSetup = () => {
console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n');
return inquirer.prompt([
{
type: 'input',
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName,
validate: (input) => {
if (input && input.length > 0) {
return true;
}
return 'Organization Name is required.';
}
},
{
type: 'list',
choices: SettingModel.MODERATION_OPTIONS,
name: 'moderation',
default: settings.moderation,
message: 'Select a moderation mode'
},
{
type: 'confirm',
name: 'requireEmailConfirmation',
default: settings.requireEmailConfirmation,
message: 'Should emails always be confirmed'
}
])
.then((answers) => {
// Update the settings that were changed.
Object.keys(answers).forEach((key) => {
if (answers[key] !== undefined) {
settings[key] = answers[key];
}
if (program.defaults) {
return SettingsService
.init()
.then(() => {
console.log('Settings created.');
console.log('\nTalk is now installed!');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
return settings.save();
// Get the current settings, we are expecing an error here.
return SettingsService
.retrieve()
.then(() => {
// We should NOT have gotten a settings object, this means that the
// application is already setup. Error out here.
throw errors.ErrSettingsInit;
})
.catch((err) => {
// If the error is `not init`, then we're good, otherwise, it's something
// else.
if (err !== errors.ErrSettingsNotInit) {
throw err;
}
})
.then(() => {
// Create the base settings model.
let settings = new SettingModel();
console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n');
return inquirer.prompt([
{
type: 'input',
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName,
validate: (input) => {
if (input && input.length > 0) {
return true;
}
return 'Organization Name is required.';
}
},
{
type: 'list',
choices: SettingModel.MODERATION_OPTIONS,
name: 'moderation',
default: settings.moderation,
message: 'Select a moderation mode'
},
{
type: 'confirm',
name: 'requireEmailConfirmation',
default: settings.requireEmailConfirmation,
message: 'Should emails always be confirmed'
}
])
.then((answers) => {
// Update the settings that were changed.
Object.keys(answers).forEach((key) => {
if (answers[key] !== undefined) {
settings[key] = answers[key];
}
});
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
return inquirer.prompt([
{
type: 'input',
name: 'displayName',
message: 'Display Name',
filter: (displayName) => {
return UsersService
.isValidDisplayName(displayName, false)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'email',
message: 'Email',
format: 'email',
validate: (value) => {
if (value && value.length >= 3) {
return true;
}
return 'Email is required';
}
},
{
name: 'password',
message: 'Password',
type: 'password',
filter: (password) => {
return UsersService
.isValidPassword(password)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
filter: (confirmPassword) => {
return UsersService
.isValidPassword(confirmPassword)
.catch((err) => {
throw err.message;
});
}
},
]);
})
.then((user) => {
if (user.password !== user.confirmPassword) {
return Promise.reject(new Error('Passwords do not match'));
}
return SetupService.setup({
settings: settings.toObject(),
user: {
email: user.email,
displayName: user.displayName,
password: user.password
}
});
});
})
.then(({user}) => {
console.log('Settings created.');
console.log(`User ${user.id} created.`);
console.log('\nTalk is now installed!');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
})
.then(() => {
console.log('Talk is now installed!');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
};
// Start tthe setup process.
performSetup();
+1 -1
View File
@@ -9,7 +9,7 @@ const inquirer = require('inquirer');
const UsersService = require('../services/users');
const UserModel = require('../models/user');
const mongoose = require('../services/mongoose');
const util = require('../util');
const util = require('./util');
const Table = require('cli-table');
const validateRequired = (msg = 'Field is required', len = 1) => (input) => {
+12 -2
View File
@@ -2,7 +2,6 @@ const pkg = require('../package.json');
const dotenv = require('dotenv');
const fs = require('fs');
const program = require('commander');
const util = require('../util');
// Perform rewrites to the runtime environment variables based on the contents
// of the process.env.REWRITE_ENV if it exists. This is done here as it is the
@@ -27,6 +26,10 @@ const parseArgs = require('minimist')(process.argv.slice(2), {
}
});
/**
* If the config flag is present, then we have to load the configuration from
* the file specified. We will then load those values into the environment.
*/
if (parseArgs.config) {
let envConfig = dotenv.parse(fs.readFileSync(parseArgs.config, {encoding: 'utf8'}));
@@ -35,8 +38,15 @@ if (parseArgs.config) {
});
}
// If the `--pid` flag is used, put the current pid there.
/**
* If the pid flag is present, then we have to create a pid file at the location
* specified.
*/
if (parseArgs.pid) {
const util = require('./util');
console.log('Wrote PID');
util.pid(parseArgs.pid);
}
View File
+1 -1
View File
@@ -35,7 +35,7 @@ test:
deployment:
release:
tag: /[0-9]+(\.[0-9]+)*/
tag: /v[0-9]+(\.[0-9]+)*/
commands:
- bash ./scripts/deploy.sh
+11 -7
View File
@@ -7,15 +7,19 @@ import Configure from 'containers/Configure/Configure';
import Streams from 'containers/Streams/Streams';
import CommunityContainer from 'containers/Community/CommunityContainer';
import LayoutContainer from 'containers/LayoutContainer';
import InstallContainer from 'containers/Install/InstallContainer';
const routes = (
<Route path='/admin' component={LayoutContainer}>
<IndexRoute component={ModerationContainer} />
<Route path='embed' component={CommentStream} />
<Route path='community' component={CommunityContainer} />
<Route path='configure' component={Configure} />
<Route path='streams' component={Streams} />
</Route>
<div>
<Route exact path="/admin/install" component={InstallContainer}/>
<Route path='/admin' component={LayoutContainer}>
<IndexRoute component={ModerationContainer} />
<Route path='embed' component={CommentStream} />
<Route path='community' component={CommunityContainer} />
<Route path='configure' component={Configure} />
<Route path='streams' component={Streams} />
</Route>
</div>
);
const AppRouter = () => <Router history={browserHistory} routes={routes} />;
+2 -2
View File
@@ -15,7 +15,7 @@ export const fetchModerationQueueComments = () => {
return Promise.all([
coralApi('/queue/comments/premod'),
coralApi('/queue/users/pending'),
coralApi('/queue/users/flagged'),
coralApi('/queue/comments/rejected'),
coralApi('/queue/comments/flagged')
])
@@ -46,7 +46,7 @@ export const fetchPendingUsersQueue = () => {
return dispatch => {
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
return coralApi('/queue/users/pending')
return coralApi('/queue/users/flagged')
.then(addUsersCommentsActions.bind(this, dispatch));
};
};
+109
View File
@@ -0,0 +1,109 @@
import coralApi from 'coral-framework/helpers/response';
import * as actions from '../constants/install';
import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
export const nextStep = () => ({type: actions.NEXT_STEP});
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
export const goToStep = step => ({type: actions.GO_TO_STEP, step});
const installRequest = () => ({type: actions.INSTALL_REQUEST});
const installSuccess = () => ({type: actions.INSTALL_SUCCESS});
const installFailure = error => ({type: actions.INSTALL_FAILURE, error});
const addError = (name, error) => ({type: actions.ADD_ERROR, name, error});
const hasError = error => ({type: actions.HAS_ERROR, error});
const clearErrors = () => ({type: actions.CLEAR_ERRORS});
const validation = (formData, dispatch, next) => {
if (!(formData != null)) {
return dispatch(hasError());
}
// Required Validation
const empty = Object.keys(formData).filter(name => {
const cond = !formData[name].length;
if (cond) {
// Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
}
return cond;
});
if (empty.length) {
return dispatch(hasError());
}
// RegExp Validation
const validation = Object.keys(formData).filter(name => {
const cond = !validate[name](formData[name]);
if (cond) {
// Adding Error
dispatch(addError(name, errorMsj[name]));
} else {
dispatch(addError(name, ''));
}
return cond;
});
if (validation.length) {
return dispatch(hasError());
}
dispatch(clearErrors());
next();
};
export const submitSettings = () => (dispatch, getState) => {
const settingsFormData = getState().install.toJS().data.settings;
validation(settingsFormData, dispatch, function() {
dispatch(nextStep());
});
};
export const submitUser = () => (dispatch, getState) => {
const userFormData = getState().install.toJS().data.user;
validation(userFormData, dispatch, function() {
const data = getState().install.toJS().data;
dispatch(installRequest());
coralApi('/setup', {method: 'POST', body: data})
.then(result => {
console.log(result);
dispatch(installSuccess());
dispatch(nextStep());
})
.catch(error => {
console.error(error);
dispatch(installFailure(`${error.message}`));
});
});
};
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
const checkInstallSuccess = installed => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
const checkInstallFailure = error => ({type: actions.CHECK_INSTALL_FAILURE, error});
export const checkInstall = next => dispatch => {
dispatch(checkInstallRequest());
coralApi('/setup')
.then(({installed}) => {
dispatch(checkInstallSuccess(installed));
if (installed) {
next();
}
})
.catch(error => {
console.error(error);
dispatch(checkInstallFailure(`${error.message}`));
});
};
+8
View File
@@ -21,3 +21,11 @@ export const sendNotificationEmail = (userId, subject, body) => {
.catch(error => dispatch({type: userTypes.USER_EMAIL_FAILURE, error}));
};
};
// let a user edit their username
export const enableUsernameEdit = (userId) => {
return (dispatch) => {
return coralApi(`/users/${userId}/username-enable`, {method: 'POST'})
.catch(error => dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error}));
};
};
@@ -3,7 +3,7 @@ import styles from './ModerationList.css';
import key from 'keymaster';
import Hammer from 'hammerjs';
import Comment from './Comment';
import UserAction from './UserAction';
import User from './User';
import SuspendUserModal from './SuspendUserModal';
// Each action has different meaning and configuration
@@ -138,7 +138,7 @@ export default class ModerationList extends React.Component {
if (menuOption === 'REJECTED') {
this.setState({suspendUserModal: action});
} else if (menuOption === 'ACCEPTED') {
this.props.userStatusUpdate('ACTIVE', action.item_id);
this.props.userStatusUpdate('APPROVED', action.item_id);
}
}
}
@@ -177,7 +177,7 @@ export default class ModerationList extends React.Component {
// If the item is an action...
const user = users[item.item_id];
modItem = user && <UserAction
modItem = user && <User
suspectWords={suspectWords}
action={item}
user={user}
@@ -36,7 +36,7 @@ class SuspendUserModal extends Component {
}
componentDidMount() {
const about = this.props.actionType === 'flag_bio' ? lang.t('suspenduser.bio') : lang.t('suspenduser.username');
const about = lang.t('suspenduser.username');
this.setState({email: lang.t('suspenduser.email', about)});
}
@@ -1,22 +1,16 @@
import React from 'react';
import Linkify from 'react-linkify';
import styles from './ModerationList.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import {Icon} from 'react-mdl';
import Highlighter from 'react-highlight-words';
import ActionButton from './ActionButton';
const linkify = new Linkify();
// Render a single comment for the list
const UserAction = props => {
const User = props => {
const {action, user} = props;
let userStatus = user.status;
const links = user.settings.bio ? linkify.getMatches(user.settings.bio) : [];
// Do not display unless the user status is 'pending' or 'banned'.
// This means that they have already been reviewed and approved.
@@ -27,8 +21,6 @@ const UserAction = props => {
<span>{user.displayName}</span>
</div>
<div className={styles.sideActions}>
{links ?
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={`actions ${styles.actions}`}>
{props.modActions.map(
(action, i) =>
@@ -48,32 +40,12 @@ const UserAction = props => {
<span className={styles.banned}><Icon name='error_outline'/> {lang.t('comment.banned_user')}</span> : null}
</div>
</div>
{
user.settings.bio &&
<div>
<div className={styles.itemBody}>
<div>{lang.t('user.user_bio')}:</div>
<span className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
<Highlighter
searchWords={props.suspectWords}
textToHighlight={user.settings.bio} />
</Linkify>
</span>
</div>
</div>
}
<div className={styles.flagCount}>
{`${action.count} ${action.action_type === 'flag_bio' ? lang.t('user.bio_flags') : lang.t('user.username_flags')}`}
</div>
</li>;
};
export default UserAction;
const linkStyles = {
backgroundColor: 'rgb(255, 219, 135)',
padding: '1px 2px'
};
export default User;
const lang = new I18n(translations);
+40 -25
View File
@@ -6,34 +6,49 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import {Logo} from './Logo';
export default ({handleLogout}) => (
export default ({handleLogout, restricted = false}) => (
<Header className={styles.header}>
<Logo />
<Navigation className={styles.nav}>
<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>
{
!restricted ?
<div>
<Navigation className={styles.nav}>
<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>
<Link className={styles.navLink} to="/admin/streams"
activeClassName={styles.active}>{lang.t('configure.streams')}</Link>
</Navigation>
<div className={styles.rightPanel}>
<ul>
<li className={styles.settings}>
<div>
<IconButton name="settings" id="menu-settings"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={handleLogout}>Sign Out</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
</ul>
</div>
activeClassName={styles.active}>
{lang.t('configure.streams')}
</Link>
</Navigation>
<div className={styles.rightPanel}>
<ul>
<li className={styles.settings}>
<div>
<IconButton name="settings" id="menu-settings"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={handleLogout}>Sign Out</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
</ul>
</div>
</div>
:
null
}
</Header>
);
@@ -0,0 +1,16 @@
export const NEXT_STEP = 'NEXT_STEP';
export const GO_TO_STEP = 'GO_TO_STEP';
export const PREVIOUS_STEP = 'PREVIOUS_STEP';
export const ADD_ERROR = 'ADD_ERROR';
export const HAS_ERROR = 'HAS_ERROR';
export const SHOW_ERRORS = 'SHOW_ERRORS';
export const CLEAR_ERRORS = 'CLEAR_ERRORS';
export const INSTALL_REQUEST = 'INSTALL_REQUEST';
export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
export const INSTALL_FAILURE = 'INSTALL_FAILURE';
export const UPDATE_FORMDATA_USER = 'UPDATE_FORMDATA_USER';
export const UPDATE_FORMDATA_SETTINGS = 'UPDATE_FORMDATA_SETTINGS';
export const CHECK_INSTALL_REQUEST = 'CHECK_INSTALL_REQUEST';
export const CHECK_INSTALL_SUCCESS = 'CHECK_INSTALL_SUCCESS';
export const CHECK_INSTALL_FAILURE = 'CHECK_INSTALL_FAILURE';
@@ -2,4 +2,5 @@ export const UPDATE_STATUS_REQUEST = 'UPDATE_STATUS_REQUEST';
export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS';
export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE';
export const USER_EMAIL_FAILURE = 'USER_EMAIL_FAILURE';
export const USERNAME_ENABLE_FAILURE = 'USERNAME_ENABLE_FAILURE';
export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS';
@@ -25,8 +25,24 @@ class EmbedLink extends Component {
}
render () {
const embedText = `<div id='coralStreamEmbed'></div><script type='text/javascript' src='${window.location.protocol}//pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embed/stream', {title: 'Comments'});</script>`;
const location = window.location;
const talkBaseUrl = [
location.protocol,
'//',
location.hostname,
location.port ? (`:${ window.location.port}`) : ''
].join('');
const coralJsUrl = [talkBaseUrl, '/embed.js'].join('');
const nonce = String(Math.random()).slice(2);
const streamElementId = `coral_talk_${nonce}`;
const embedText = `
<div id="${streamElementId}"></div>
<script src="${coralJsUrl}" async onload="
Coral.Talk.render(document.getElementById('${streamElementId}'), {
talk: '${talkBaseUrl}'
});
"></script>
`.trim();
return (
<div>
<h3>{this.props.title}</h3>
@@ -0,0 +1,93 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import styles from './style.css';
import {Wizard, WizardNav} from 'coral-ui';
import {Layout} from '../../components/ui/Layout';
import {
nextStep,
previousStep,
goToStep,
updateUserFormData,
updateSettingsFormData,
submitSettings,
submitUser,
checkInstall
} from '../../actions/install';
import InitialStep from './components/Steps/InitialStep';
import AddOrganizationName from './components/Steps/AddOrganizationName';
import CreateYourAccount from './components/Steps/CreateYourAccount';
import FinalStep from './components/Steps/FinalStep';
class InstallContainer extends Component {
componentDidMount() {
const {checkInstall} = this.props;
checkInstall(() => {
this.context.router.push('/admin');
});
}
render() {
const {install} = this.props;
return (
<Layout restricted={true}>
<div className={styles.Install}>
{
!install.alreadyInstalled ? (
<div>
<h2>Welcome to the Coral Project</h2>
{ install.step !== 0 ? <WizardNav items={install.navItems} currentStep={install.step} icon='check'/> : null }
<Wizard currentStep={install.step} {...this.props}>
<InitialStep/>
<AddOrganizationName/>
<CreateYourAccount/>
<FinalStep/>
</Wizard>
</div>
) : (
<div>Talk is already installed</div>
)
}
</div>
</Layout>
);
}
}
InstallContainer.contextTypes = {
router: React.PropTypes.object
};
const mapStateToProps = state => ({
install: state.install.toJS()
});
const mapDispatchToProps = dispatch => ({
checkInstall: next => dispatch(checkInstall(next)),
nextStep: () => dispatch(nextStep()),
previousStep: () => dispatch(previousStep()),
goToStep: step => dispatch(goToStep(step)),
handleSettingsChange: e => {
const {name, value} = e.currentTarget;
dispatch(updateSettingsFormData(name, value));
},
handleUserChange: e => {
const {name, value} = e.currentTarget;
dispatch(updateUserFormData(name, value));
},
handleSettingsSubmit: e => {
e.preventDefault();
dispatch(submitSettings());
},
handleUserSubmit: e => {
e.preventDefault();
dispatch(submitUser());
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(InstallContainer);
@@ -0,0 +1,30 @@
import React from 'react';
import styles from './style.css';
import {FormField, Button} from 'coral-ui';
const AddOrganizationName = props => {
const {handleSettingsChange, handleSettingsSubmit, install} = props;
return (
<div className={styles.step}>
<p>
Please tell us the name of your organization. This will appear in emails when
inviting new team members
</p>
<div className={styles.form}>
<form onSubmit={handleSettingsSubmit}>
<FormField
className={styles.FormField}
id="organizationName"
type="text"
label='Organization name'
onChange={handleSettingsChange}
showErrors={install.showErrors}
errorMsg={install.errors.organizationName} />
<Button type="submit" cStyle='black' full>Save</Button>
</form>
</div>
</div>
);
};
export default AddOrganizationName;
@@ -0,0 +1,65 @@
import React from 'react';
import styles from './style.css';
import {FormField, Button, Spinner} from 'coral-ui';
const InitialStep = props => {
const {handleUserChange, handleUserSubmit, install} = props;
return (
<div className={styles.step}>
<div className={styles.form}>
<form onSubmit={handleUserSubmit}>
<FormField
className={styles.formField}
id="email"
type="email"
label='Email address'
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.email}
noValidate
/>
<FormField
className={styles.formField}
id="displayName"
type="text"
label='Username'
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.displayName}
/>
<FormField
className={styles.formField}
id="password"
type="password"
label='Password'
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.password}
/>
<FormField
className={styles.formField}
id="confirmPassword"
type="password"
label='Confirm Password'
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.confirmPassword}
/>
{
!props.install.isLoading ?
<Button cStyle='black' type="submit" full>Save</Button>
:
<Spinner />
}
{props.install.installRequest === 'FAILURE' && <div className={styles.error}>Error: {props.install.installRequestError}</div>}
</form>
</div>
</div>
);
};
export default InitialStep;
@@ -0,0 +1,20 @@
import React from 'react';
import styles from './style.css';
import {Button} from 'coral-ui';
import {Link} from 'react-router';
const InitialStep = () => {
return (
<div className={`${styles.step} ${styles.finalStep}`}>
<p>
Thanks for installing Talk! We sent an email to verify your email
address. While you finish setting the account, you can start engaging
with your readers now.
</p>
<Button raised><Link to='/admin'>Launch Talk</Link></Button>
<Button cStyle='black' raised><a href="http://coralproject.net">Close this Installer</a></Button>
</div>
);
};
export default InitialStep;
@@ -0,0 +1,19 @@
import React from 'react';
import styles from './style.css';
import {Button} from 'coral-ui';
const InitialStep = props => {
const {nextStep} = props;
return (
<div className={styles.step}>
<p>
The remainder of the Talk installation will take about ten minutes.
Once you complete the following two steps, you will have a free
installation and provision of Mongo and Redis.
</p>
<Button cStyle='green' onClick={nextStep} raised>Get Started</Button>
</div>
);
};
export default InitialStep;
@@ -0,0 +1,44 @@
import React from 'react';
import styles from './style.css';
import {Button, Select, Option, FormField} from 'coral-ui';
const InviteTeamMembers = props => {
const {nextStep} = props;
return (
<div className={styles.step}>
<h3>Invite Team Members </h3>
<p>
Once registered, new team members will receive an email to Create
their password.
</p>
<div className={styles.form}>
<form>
<FormField
className={styles.formField}
id="email"
type="email"
label='Email address' required/>
<FormField
className={styles.formField}
id="username"
type="text"
label='Username' required/>
<div className={styles.formField}>
<label htmlFor='role'>Assing a role</label>
<Select id='role' label='Select Role'>
<Option>Admin</Option>
<Option>Moderator</Option>
</Select>
</div>
<Button cStyle='black' onClick={nextStep} icon='arrow_forward' full>Invite team member</Button>
</form>
</div>
</div>
);
};
export default InviteTeamMembers;
@@ -0,0 +1,77 @@
.step {
padding: 20px 0;
h3 {
max-width: 450px;
margin: 0 auto;
text-align: left;
font-size: 1.4em;
font-weight: bold;
}
p {
max-width: 450px;
margin: 0 auto 30px;
font-size: 1.1em;
text-align: left;
}
> button {
min-width: 145px;
a {
text-decoration: none;
color: inherit;
}
}
.form {
max-width: 300px;
margin: 30px auto;
text-align: left;
form > button {
margin: 30px 0;
}
.formField {
text-align: left;
label {
text-align: left;
display: block;
margin: 10px 0;
font-weight: 400;
font-size: 1.08em;
}
> input {
border: solid 1px rgba(0, 0, 0, 0.23);
padding: 10px;
border-radius: 3px;
width: 100%;
box-sizing: border-box;
transition: border-color 0.2s ease;
font-size: 1em;
&:focus {
border-color: black;
}
}
}
}
}
.finalStep {
button {
width: 225px;
margin-right: 10px;
}
}
.error {
background: #FFEBEE;
color: #B71C1C;
padding: 10px;
margin-bottom: 20px;
border-radius: 2px;
}
@@ -0,0 +1,12 @@
.Install {
max-width: 900px;
margin: 0 auto;
text-align: center;
padding: 50px 0;
h2 {
font-size: 2em;
font-weight: 500;
margin: 0;
}
}
@@ -1,7 +1,9 @@
import React from 'react';
import React, {Component} from 'react';
import key from 'keymaster';
import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import {Spinner} from 'coral-ui';
import {withRouter} from 'react-router';
import {modQueueQuery} from '../../graphql/queries';
@@ -20,8 +22,9 @@ import {userStatusUpdate, sendNotificationEmail} from 'actions/users';
import {setActiveTab, toggleModal, singleView} from 'actions/moderation';
import ModerationQueue from './ModerationQueue';
import ModerationQueueHeader from './components/ModerationQueueHeader';
class ModerationContainer extends React.Component {
class ModerationContainer extends Component {
componentWillMount() {
const {toggleModal, singleView} = this.props;
@@ -43,16 +46,6 @@ class ModerationContainer extends React.Component {
onTabClick = (activeTab) => {
const {setActiveTab} = this.props;
setActiveTab(activeTab);
if (activeTab === 'premod') {
this.props.fetchPremodQueue();
} else if (activeTab === 'rejected') {
this.props.fetchRejectedQueue();
} else if (activeTab === 'flagged') {
this.props.fetchFlaggedQueue();
} else {
this.props.fetchModerationQueueComments();
}
}
onClose = () => {
@@ -61,40 +54,32 @@ class ModerationContainer extends React.Component {
}
render () {
const {comments, actions, settings, moderation} = this.props;
const {data, moderation} = this.props;
// Remove all this filters
const premodIds = comments.ids.filter(id => comments.byId[id].status === 'PREMOD');
const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'REJECTED');
const flaggedIds = comments.ids.filter(id =>
comments.byId[id].flagged === true &&
comments.byId[id].status !== 'REJECTED' &&
comments.byId[id].status !== 'ACCEPTED'
);
const userActionIds = actions.ids.filter(id => actions.byId[id].item_type === 'USERS');
// show the Pre-Mod tab if premod is enabled globally OR there are pre-mod comments in the db.
let enablePremodTab = (settings.settings && settings.settings.moderation === 'PRE') || premodIds.length;
if (data.loading) {
return <div><Spinner/></div>;
}
return (
<ModerationQueue
enablePremodTab={enablePremodTab}
onTabClick={this.onTabClick}
onClose={this.onClose}
premodIds={premodIds}
userActionIds={userActionIds}
rejectedIds={rejectedIds}
flaggedIds={flaggedIds}
{...this.props}
{...moderation}
/>
<div>
<ModerationQueueHeader onTabClick={this.onTabClick} {...moderation} />
<ModerationQueue
activeTab={moderation.activeTab}
data={data}
onTabClick={this.onTabClick}
onClose={this.onClose}
{...this.props}
{...moderation}
/>
</div>
);
}
}
ModerationContainer.contextTypes = {
router: React.PropTypes.func.isRequired
};
// ModerationContainer.contextTypes = {
// router: React.PropTypes.func.isRequired
// };
const mapStateToProps = state => ({
moderation: state.moderation.toJS(),
@@ -120,13 +105,14 @@ const mapDispatchToProps = dispatch => ({
dispatch(fetchModerationQueueComments());
}),
suspendUser: (userId, subject, text) => dispatch(userStatusUpdate('suspended', userId))
.then(() => dispatch(sendNotificationEmail(userId, subject, text)))
.then(() => dispatch(fetchModerationQueueComments()))
.then(() => dispatch(sendNotificationEmail(userId, subject, text)))
.then(() => dispatch(fetchModerationQueueComments()))
,
updateStatus: (action, comment) => dispatch(updateStatus(action, comment))
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withRouter,
modQueueQuery
)(ModerationContainer);
@@ -0,0 +1,132 @@
import React from 'react';
import key from 'keymaster';
import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import {modQueueQuery} from '../../graphql/queries';
import {
updateStatus,
showBanUserDialog,
hideBanUserDialog,
fetchPremodQueue,
fetchRejectedQueue,
fetchFlaggedQueue,
fetchModerationQueueComments,
} from 'actions/comments';
import {fetchSettings} from 'actions/settings';
import {userStatusUpdate, sendNotificationEmail} from 'actions/users';
import {setActiveTab, toggleModal, singleView} from 'actions/moderation';
import ModerationQueue from './ModerationQueue';
class ModerationContainer extends React.Component {
componentWillMount() {
const {toggleModal, singleView} = this.props;
this.props.fetchModerationQueueComments();
this.props.fetchSettings();
key('s', () => singleView());
key('shift+/', () => toggleModal(true));
key('esc', () => toggleModal(false));
}
componentWillUnmount() {
key.unbind('s');
key.unbind('shift+/');
key.unbind('esc');
}
onTabClick = (activeTab) => {
const {setActiveTab} = this.props;
setActiveTab(activeTab);
if (activeTab === 'premod') {
this.props.fetchPremodQueue();
} else if (activeTab === 'rejected') {
this.props.fetchRejectedQueue();
} else if (activeTab === 'flagged') {
this.props.fetchFlaggedQueue();
} else {
this.props.fetchModerationQueueComments();
}
}
onClose = () => {
const {toggleModal} = this.props;
toggleModal(false);
}
render () {
const {comments, actions, settings, moderation} = this.props;
// Remove all this filters
const premodIds = comments.ids.filter(id => comments.byId[id].status === 'PREMOD');
const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'REJECTED');
const flaggedIds = comments.ids.filter(id =>
comments.byId[id].flagged === true &&
comments.byId[id].status !== 'REJECTED' &&
comments.byId[id].status !== 'ACCEPTED'
);
const userActionIds = actions.ids.filter(id => actions.byId[id].item_type === 'USERS');
// show the Pre-Mod tab if premod is enabled globally OR there are pre-mod comments in the db.
let enablePremodTab = (settings.settings && settings.settings.moderation === 'PRE') || premodIds.length;
return (
<ModerationQueue
enablePremodTab={enablePremodTab}
onTabClick={this.onTabClick}
onClose={this.onClose}
premodIds={premodIds}
userActionIds={userActionIds}
rejectedIds={rejectedIds}
flaggedIds={flaggedIds}
{...this.props}
{...moderation}
/>
);
}
}
ModerationContainer.contextTypes = {
router: React.PropTypes.func.isRequired
};
const mapStateToProps = state => ({
moderation: state.moderation.toJS(),
comments: state.comments.toJS(),
settings: state.settings.toJS(),
users: state.users.toJS(),
actions: state.actions.toJS()
});
const mapDispatchToProps = dispatch => ({
setActiveTab: tab => dispatch(setActiveTab(tab)),
toggleModal: open => dispatch(toggleModal(open)),
singleView: () => dispatch(singleView()),
fetchSettings: () => dispatch(fetchSettings()),
fetchModerationQueueComments: () => dispatch(fetchModerationQueueComments()),
fetchPremodQueue: () => dispatch(fetchPremodQueue()),
fetchRejectedQueue: () => dispatch(fetchRejectedQueue()),
fetchFlaggedQueue: () => dispatch(fetchFlaggedQueue()),
showBanUserDialog: (userId, userName, commentId) => dispatch(showBanUserDialog(userId, userName, commentId)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
userStatusUpdate: (status, userId, commentId) => dispatch(userStatusUpdate(status, userId, commentId)).then(() => {
dispatch(fetchModerationQueueComments());
}),
suspendUser: (userId, subject, text) => dispatch(userStatusUpdate('suspended', userId))
.then(() => dispatch(sendNotificationEmail(userId, subject, text)))
.then(() => dispatch(fetchModerationQueueComments()))
,
updateStatus: (action, comment) => dispatch(updateStatus(action, comment))
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
modQueueQuery
)(ModerationContainer);
@@ -1,212 +1,22 @@
import React, {PropTypes} from 'react';
import styles from './ModerationQueue.css';
import React from 'react';
import ModerationKeysModal from 'components/ModerationKeysModal';
import ModerationList from 'components/ModerationList';
import BanUserDialog from 'components/BanUserDialog';
// import Comment from '../../components/Comment';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
const lang = new I18n(translations);
const ModerationQueue = (props) => (
<div>
<div className='mdl-tabs'>
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<a href='#all'
onClick={(e) => {
e.preventDefault();
props.onTabClick('all');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'all' ? styles.active : ''}`}
>
{lang.t('modqueue.all')}
</a>
{
props.enablePremodTab
? <a href='#premod'
onClick={(e) => {
e.preventDefault();
props.onTabClick('premod');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'premod' ? styles.active : ''}`}>
{lang.t('modqueue.premod')}
</a>
: null
}
<a href='#account'
onClick={(e) => {
e.preventDefault();
props.onTabClick('account');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'account' ? styles.active : ''}`}>
{lang.t('modqueue.account')}
</a>
<a href='#rejected'
onClick={(e) => {
e.preventDefault();
props.onTabClick('rejected');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'rejected' ? styles.active : ''}`}
>
{lang.t('modqueue.rejected')}
</a>
<a href='#flagged'
onClick={(e) => {
e.preventDefault();
props.onTabClick('flagged');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'flagged' ? styles.active : ''}`}
>
{lang.t('modqueue.flagged')}
</a>
</div>
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='all'>
{
props.activeTab === 'all' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'all'}
singleView={props.singleView}
commentIds={[...props.premodIds, ...props.flaggedIds]}
comments={props.comments.byId}
users={props.users.byId}
actionIds={props.userActionIds}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
export default (props) => {
return (
<div>
{/* <Comment*/}
{/* comment={comment}*/}
{/* key={i}*/}
{/* author={comment.user}*/}
{/* />*/}
{
props.enablePremodTab
? <div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='premod'>
{
props.activeTab === 'premod' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'premod'}
singleView={props.singleView}
commentIds={props.premodIds}
comments={props.comments.byId}
users={props.users.byId}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
: null
props.data[props.activeTab].map((comment, i) =>
<div key={i}>
{comment.body}
</div>
)
}
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='account'>
{
props.activeTab === 'account' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'account'}
singleView={props.singleView}
users={props.users.byId}
actionIds={props.userActionIds}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
{
props.activeTab === 'flagged' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'flagged'}
singleView={props.singleView}
commentIds={props.flaggedIds}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
comments={props.comments.byId}
users={props.users.byId}
updateCommentStatus={props.updateStatus}
modActions={['reject', 'approve']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
{
props.activeTab === 'rejected' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'rejected'}
singleView={props.singleView}
commentIds={props.rejectedIds}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
comments={props.comments.byId}
users={props.users.byId}
updateCommentStatus={props.updateStatus}
modActions={['approve']}
loading={props.comments.loading}
/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<ModerationKeysModal open={props.modalOpen} onClose={props.closeModal} />
</div>
</div>
);
ModerationQueue.propTypes = {
enablePremodTab: PropTypes.bool.isRequired
);
};
export default ModerationQueue;
@@ -0,0 +1,212 @@
import React, {PropTypes} from 'react';
import styles from './ModerationQueue.css';
import ModerationKeysModal from 'components/ModerationKeysModal';
import ModerationList from 'components/ModerationList';
import BanUserDialog from 'components/BanUserDialog';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
const lang = new I18n(translations);
const ModerationQueue = (props) => (
<div>
<div className='mdl-tabs'>
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<a href='#all'
onClick={(e) => {
e.preventDefault();
props.onTabClick('all');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'all' ? styles.active : ''}`}
>
{lang.t('modqueue.all')}
</a>
{
props.enablePremodTab
? <a href='#premod'
onClick={(e) => {
e.preventDefault();
props.onTabClick('premod');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'premod' ? styles.active : ''}`}>
{lang.t('modqueue.premod')}
</a>
: null
}
<a href='#account'
onClick={(e) => {
e.preventDefault();
props.onTabClick('account');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'account' ? styles.active : ''}`}>
{lang.t('modqueue.account')}
</a>
<a href='#rejected'
onClick={(e) => {
e.preventDefault();
props.onTabClick('rejected');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'rejected' ? styles.active : ''}`}
>
{lang.t('modqueue.rejected')}
</a>
<a href='#flagged'
onClick={(e) => {
e.preventDefault();
props.onTabClick('flagged');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'flagged' ? styles.active : ''}`}
>
{lang.t('modqueue.flagged')}
</a>
</div>
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='all'>
{
props.activeTab === 'all' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'all'}
singleView={props.singleView}
commentIds={[...props.premodIds, ...props.flaggedIds]}
comments={props.comments.byId}
users={props.users.byId}
actionIds={props.userActionIds}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
{
props.enablePremodTab
? <div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='premod'>
{
props.activeTab === 'premod' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'premod'}
singleView={props.singleView}
commentIds={props.premodIds}
comments={props.comments.byId}
users={props.users.byId}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
: null
}
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='account'>
{
props.activeTab === 'account' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'account'}
singleView={props.singleView}
users={props.users.byId}
actionIds={props.userActionIds}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
{
props.activeTab === 'flagged' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'flagged'}
singleView={props.singleView}
commentIds={props.flaggedIds}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
comments={props.comments.byId}
users={props.users.byId}
updateCommentStatus={props.updateStatus}
modActions={['reject', 'approve']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
{
props.activeTab === 'rejected' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'rejected'}
singleView={props.singleView}
commentIds={props.rejectedIds}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
comments={props.comments.byId}
users={props.users.byId}
updateCommentStatus={props.updateStatus}
modActions={['approve']}
loading={props.comments.loading}
/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<ModerationKeysModal open={props.modalOpen} onClose={props.closeModal} />
</div>
</div>
);
ModerationQueue.propTypes = {
enablePremodTab: PropTypes.bool.isRequired
};
export default ModerationQueue;
@@ -0,0 +1,62 @@
import React from 'react';
import styles from '../ModerationQueue.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../../translations.json';
const lang = new I18n(translations);
const ModerationQueueHeader = (props) => (
<div className='mdl-tabs'>
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<a href='#all'
onClick={(e) => {
e.preventDefault();
props.onTabClick('all');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'all' ? styles.active : ''}`}
>
{lang.t('modqueue.all')}
</a>
{
props.enablePremodTab
? <a href='#premod'
onClick={(e) => {
e.preventDefault();
props.onTabClick('premod');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'premod' ? styles.active : ''}`}>
{lang.t('modqueue.premod')}
</a>
: null
}
<a href='#account'
onClick={(e) => {
e.preventDefault();
props.onTabClick('account');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'account' ? styles.active : ''}`}>
{lang.t('modqueue.account')}
</a>
<a href='#rejected'
onClick={(e) => {
e.preventDefault();
props.onTabClick('rejected');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'rejected' ? styles.active : ''}`}
>
{lang.t('modqueue.rejected')}
</a>
<a href='#flagged'
onClick={(e) => {
e.preventDefault();
props.onTabClick('flagged');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'flagged' ? styles.active : ''}`}
>
{lang.t('modqueue.flagged')}
</a>
</div>
</div>
);
export default ModerationQueueHeader;
@@ -0,0 +1,18 @@
fragment commentView on Comment {
id
body
created_at
status
user {
id
name: displayName
}
actions {
type: action_type
count
current: current_user {
id
created_at
}
}
}
@@ -4,7 +4,7 @@ import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
options: () => ({
variables: {
asset_url: 'id'
asset_id: 'bae04102-33b5-4f43-b827-45d9ba9f2b37'
}
})
});
@@ -1,20 +1,34 @@
query ModQueue {
flagged_comments: comments(query: {
#import "../fragments/commentView.graphql"
query ($asset_id: ID!) {
premod: comments(query: {
statuses: [PREMOD],
asset_id: $asset_id
}) {
...commentView
}
flagged: comments(query: {
action_type: FLAG,
asset_id: $asset_id
}) {
id
}
accepted_comments: comments(query: {
}) {
...commentView
}
all: comments(query: {
statuses: [ACCEPTED],
asset_id: $asset_id
}) {
id
}
rejected_comments: comments(query: {
}) {
...commentView
}
account: comments(query: {
statuses: [ACCEPTED],
asset_id: $asset_id
}) {
...commentView
}
rejected: comments(query: {
statuses: [REJECTED],
asset_id: $asset_id
}) {
id
}
}) {
...commentView
}
}
@@ -0,0 +1,91 @@
import {Map} from 'immutable';
import * as actions from '../constants/install';
const initialState = Map({
isLoading: false,
data: Map({
settings: Map({
organizationName: ''
}),
user: Map({
displayName: '',
email: '',
password: '',
confirmPassword: ''
})
}),
errors: Map({
organizationName: '',
displayName: '',
email: '',
password: '',
confirmPassword: ''
}),
showErrors: false,
hasError: false,
error: null,
step: 0,
navItems: [{
text: '1. Add Organization Name',
step: 1
},
{
text: '2. Create your account',
step: 2
}],
installRequest: null,
installRequestError: null,
alreadyInstalled: false
});
export default function install (state = initialState, action) {
switch (action.type) {
case actions.NEXT_STEP:
return state
.set('step', state.get('step') + 1);
case actions.PREVIOUS_STEP:
return state
.set('step', state.get('step') - 1);
case actions.GO_TO_STEP:
return state
.set('step', action.step);
case actions.UPDATE_FORMDATA_SETTINGS:
return state
.setIn(['data', 'settings', action.name], action.value);
case actions.UPDATE_FORMDATA_USER:
return state
.setIn(['data', 'user', action.name], action.value);
case actions.HAS_ERROR:
return state
.merge({
hasError: true,
showErrors: true
});
case actions.ADD_ERROR:
return state
.setIn(['errors', action.name], action.error);
case actions.CLEAR_ERRORS:
return state
.set('errors', Map());
case actions.INSTALL_REQUEST:
return state
.set('isLoading', true);
case actions.INSTALL_SUCCESS:
return state
.set('isLoading', false)
.set('installRequest', 'SUCCESS');
case actions.INSTALL_FAILURE:
return state
.merge({
isLoading: false,
installRequest: 'FAILURE',
installRequestError: action.error
});
case actions.CHECK_INSTALL_SUCCESS:
return state
.set('alreadyInstalled', action.installed);
default :
return state;
}
}
+1 -1
View File
@@ -50,7 +50,7 @@
"configure": {
"enable-pre-moderation": "Enable pre-moderation",
"enable-pre-moderation-text": "Moderators must approve any comment before it is published.",
"require-email-verification": "Require Email Confirmation",
"require-email-verification": "Require Email Verification",
"require-email-verification-text": "New Users must verify their email before commenting",
"include-comment-stream": "Include Comment Stream Description for Readers.",
"include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.",
+1 -8
View File
@@ -94,14 +94,7 @@ class Comment extends React.Component {
style={{marginLeft: depth * 30}}>
<hr aria-hidden={true} />
<AuthorName
author={comment.user}
addNotification={this.props.addNotification}
id={comment.id}
author_id={comment.user.id}
postAction={this.props.postAction}
showSignInDialog={this.props.showSignInDialog}
deleteAction={this.props.deleteAction}
currentUser={currentUser}/>
author={comment.user}/>
<PubDate created_at={comment.created_at} />
<Content body={comment.body} />
<div className="commentActionsLeft">
+20 -10
View File
@@ -11,6 +11,7 @@ const {fetchAssetSuccess} = assetActions;
import {queryStream} from 'coral-framework/graphql/queries';
import {postComment, postAction, deleteAction} from 'coral-framework/graphql/mutations';
import {editName} from 'coral-framework/actions/user';
import {Notification, notificationActions, authActions, assetActions, pym} from 'coral-framework';
import Stream from './Stream';
@@ -20,6 +21,7 @@ import CommentBox from 'coral-plugin-commentbox/CommentBox';
import UserBox from 'coral-sign-in/components/UserBox';
import SignInContainer from 'coral-sign-in/containers/SignInContainer';
import SuspendedAccount from 'coral-framework/components/SuspendedAccount';
import ChangeDisplayNameContainer from '../../coral-sign-in/containers/ChangeDisplayNameContainer';
import SettingsContainer from 'coral-settings/containers/SettingsContainer';
import RestrictedContent from 'coral-framework/components/RestrictedContent';
import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer';
@@ -83,6 +85,8 @@ class Embed extends Component {
const openStream = closedAt === null;
const banned = user && user.status === 'BANNED';
const expandForLogin = showSignInDialog ? {
minHeight: document.body.scrollHeight + 200
} : {};
@@ -99,7 +103,7 @@ class Embed extends Component {
<Tab>Settings</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{loggedIn && <UserBox user={user} logout={this.props.logout} changeTab={this.changeTab} />}
{loggedIn && <UserBox user={user} logout={this.props.logout} changeTab={this.changeTab}/>}
<TabContent show={activeTab === 0}>
{
openStream
@@ -108,7 +112,12 @@ class Embed extends Component {
content={asset.settings.infoBoxContent}
enable={asset.settings.infoBoxEnable}
/>
<RestrictedContent restricted={false} restrictedComp={<SuspendedAccount />}>
<RestrictedContent restricted={banned} restrictedComp={
<SuspendedAccount
canEditName={user && user.canEditName}
editName={this.props.editName}
/>
}>
{
user
? <CommentBox
@@ -121,7 +130,6 @@ class Embed extends Component {
premod={asset.settings.moderation}
isReply={false}
currentUser={this.props.auth.user}
banned={false}
authorId={user.id}
charCount={asset.settings.charCountEnable && asset.settings.charCount} />
: null
@@ -130,7 +138,8 @@ class Embed extends Component {
</div>
: <p>{asset.settings.closedMessage}</p>
}
{!loggedIn && <SignInContainer offset={signInOffset}/>}
{!loggedIn && <SignInContainer requireEmailConfirmation={asset.settings.requireEmailConfirmation} offset={signInOffset}/>}
{loggedIn && user && <ChangeDisplayNameContainer loggedIn={loggedIn} offset={signInOffset} user={user} />}
<Stream
refetch={refetch}
addNotification={this.props.addNotification}
@@ -146,28 +155,28 @@ class Embed extends Component {
clearNotification={this.props.clearNotification}
notification={{text: null}}
/>
</TabContent>
<TabContent show={activeTab === 1}>
</TabContent>
<TabContent show={activeTab === 1}>
<SettingsContainer
loggedIn={loggedIn}
userData={this.props.userData}
showSignInDialog={this.props.showSignInDialog}
/>
</TabContent>
<TabContent show={activeTab === 2}>
</TabContent>
<TabContent show={activeTab === 2}>
<RestrictedContent restricted={!loggedIn}>
<ConfigureStreamContainer
status={status}
onClick={this.toggleStatus}
/>
</RestrictedContent>
</TabContent>
</TabContent>
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</div>
</div>
</div>
);
}
@@ -191,6 +200,7 @@ const mapDispatchToProps = dispatch => ({
});
},
clearNotification: () => dispatch(clearNotification()),
editName: (displayName) => dispatch(editName(displayName)),
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
logout: () => dispatch(logout()),
dispatch: d => dispatch(d)
File diff suppressed because one or more lines are too long
+48 -17
View File
@@ -8,6 +8,28 @@ import coralApi, {base} from '../helpers/response';
export const showSignInDialog = (offset = 0) => ({type: actions.SHOW_SIGNIN_DIALOG, offset});
export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG});
export const createDisplayNameRequest = () => ({type: actions.CREATE_DISPLAYNAME_REQUEST});
export const showCreateDisplayNameDialog = () => ({type: actions.SHOW_CREATEDISPLAYNAME_DIALOG});
export const hideCreateDisplayNameDialog = () => ({type: actions.HIDE_CREATEDISPLAYNAME_DIALOG});
const createDisplayNameSuccess = () => ({type: actions.CREATEDISPLAYNAME_SUCCESS});
const createDisplayNameFailure = error => ({type: actions.CREATEDISPLAYNAME_FAILURE, error});
export const updateDisplayName = ({displayName}) => ({type: actions.UPDATE_DISPLAYNAME, displayName});
export const createDisplayName = (userId, formData) => dispatch => {
dispatch(createDisplayNameRequest());
coralApi('/account/displayname', {method: 'PUT', body: formData})
.then(() => {
dispatch(createDisplayNameSuccess());
dispatch(hideCreateDisplayNameDialog());
dispatch(updateDisplayName(formData));
})
.catch(error => {
dispatch(createDisplayNameFailure(lang.t(`error.${error.message}`)));
});
};
export const changeView = view => dispatch =>
dispatch({
type: actions.CHANGE_VIEW,
@@ -21,7 +43,6 @@ export const cleanState = () => ({type: actions.CLEAN_STATE});
const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST});
const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin});
const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
const emailConfirmError = () => ({type: actions.EMAIL_CONFIRM_ERROR});
export const fetchSignIn = (formData) => (dispatch) => {
dispatch(signInRequest());
@@ -36,7 +57,6 @@ export const fetchSignIn = (formData) => (dispatch) => {
// the user might not have a valid email. prompt the user user re-request the confirmation email
dispatch(signInFailure(lang.t('error.emailNotVerified', error.metadata)));
dispatch(emailConfirmError());
} else {
// invalid credentials
@@ -60,6 +80,19 @@ export const fetchSignInFacebook = () => dispatch => {
);
};
// Sign Up Facebook
const signUpFacebookRequest = () => ({type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST});
export const fetchSignUpFacebook = () => dispatch => {
dispatch(signUpFacebookRequest());
window.open(
`${base}/auth/facebook`,
'Continue with Facebook',
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
);
};
export const facebookCallback = (err, data) => dispatch => {
if (err) {
signInFacebookFailure(err);
@@ -69,6 +102,7 @@ export const facebookCallback = (err, data) => dispatch => {
const user = JSON.parse(data);
dispatch(signInFacebookSuccess(user));
dispatch(hideSignInDialog());
dispatch(showCreateDisplayNameDialog());
} catch (err) {
dispatch(signInFacebookFailure(err));
return;
@@ -81,15 +115,12 @@ const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST});
const signUpSuccess = user => ({type: actions.FETCH_SIGNUP_SUCCESS, user});
const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error});
export const fetchSignUp = formData => (dispatch) => {
export const fetchSignUp = (formData, redirectUri) => (dispatch) => {
dispatch(signUpRequest());
coralApi('/users', {method: 'POST', body: formData})
coralApi('/users', {method: 'POST', body: formData, headers: {'X-Pym-Url': redirectUri}})
.then(({user}) => {
dispatch(signUpSuccess(user));
setTimeout(() =>{
dispatch(changeView('SIGNIN'));
}, 3000);
})
.catch(error => {
dispatch(signUpFailure(lang.t(`error.${error.message}`)));
@@ -150,20 +181,20 @@ export const checkLogin = () => dispatch => {
});
};
const confirmEmailRequest = () => ({type: actions.CONFIRM_EMAIL_REQUEST});
const confirmEmailSuccess = () => ({type: actions.CONFIRM_EMAIL_SUCCESS});
const confirmEmailFailure = () => ({type: actions.CONFIRM_EMAIL_FAILURE});
const verifyEmailRequest = () => ({type: actions.VERIFY_EMAIL_REQUEST});
const verifyEmailSuccess = () => ({type: actions.VERIFY_EMAIL_SUCCESS});
const verifyEmailFailure = () => ({type: actions.VERIFY_EMAIL_FAILURE});
export const requestConfirmEmail = email => dispatch => {
dispatch(confirmEmailRequest());
return coralApi('/users/resend-confirm', {method: 'POST', body: {email}})
export const requestConfirmEmail = (email, redirectUri) => dispatch => {
dispatch(verifyEmailRequest());
return coralApi('/users/resend-verify', {method: 'POST', body: {email}, headers: {'X-Pym-Url': redirectUri}})
.then(() => {
dispatch(confirmEmailSuccess());
dispatch(verifyEmailSuccess());
})
.catch(err => {
console.log('failed to send email confirmation', err);
console.log('failed to send email verification', err);
// email might have already been confirmed
dispatch(confirmEmailFailure());
// email might have already been verifyed
dispatch(verifyEmailFailure());
});
};
+4 -12
View File
@@ -1,4 +1,3 @@
import * as actions from '../constants/user';
import {addNotification} from '../actions/notification';
import coralApi from '../helpers/response';
@@ -6,16 +5,9 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
const saveBioRequest = () => ({type: actions.SAVE_BIO_REQUEST});
const saveBioSuccess = settings => ({type: actions.SAVE_BIO_SUCCESS, settings});
const saveBioFailure = error => ({type: actions.SAVE_BIO_FAILURE, error});
export const saveBio = (user_id, formData) => dispatch => {
dispatch(saveBioRequest());
coralApi('/account/settings', {method: 'PUT', body: formData})
export const editName = (displayName) => (dispatch) => {
return coralApi('/account/displayname', {method: 'PUT', body: {displayName}})
.then(() => {
dispatch(addNotification('success', lang.t('successBioUpdate')));
dispatch(saveBioSuccess(formData));
})
.catch(error => dispatch(saveBioFailure(error)));
dispatch(addNotification('success', lang.t('successNameUpdate')));
});
};
@@ -2,3 +2,13 @@
background: #D8D8D8;
padding: 25px;
}
.editNameInput {
margin-top: 10px;
margin-bottom: 10px;
}
.alert {
margin-top: 10px;
color: #B71C1C;
}
@@ -1,11 +1,79 @@
import React from 'react';
import React, {Component, PropTypes} from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-framework/translations.json';
const lang = new I18n(translations);
import styles from './RestrictedContent.css';
import {Button} from 'coral-ui';
import validate from '../helpers/validate';
export default () => (
<div className={styles.message}>
<span>{lang.t('suspendedAccountMsg')}</span>
</div>
);
class SuspendedAccount extends Component {
static propTypes = {
canEditName: PropTypes.bool,
editName: PropTypes.func.isRequired
}
state = {
displayName: '',
alert: ''
}
onSubmitClick = (e) => {
const {editName} = this.props;
const {displayName} = this.state;
e.preventDefault();
if (validate.displayName(displayName)) {
editName(displayName)
.then(() => location.reload())
.catch((error) => {
this.setState({alert: lang.t(`error.${error.message}`)});
});
} else {
this.setState({alert: lang.t('editName.error')});
}
}
render () {
const {canEditName} = this.props;
const {displayName, alert} = this.state;
return <div className={styles.message}>
<span>{
canEditName ?
lang.t('editName.msg')
: lang.t('bannedAccountMsg')
}</span>
{
canEditName ?
<div>
<div className={styles.alert}>
{alert}
</div>
<label
htmlFor='displayName'
className="screen-reader-text"
aria-hidden={true}>
{lang.t('editName.label')}
</label>
<input
type='text'
className={styles.editNameInput}
value={displayName}
placeholder={lang.t('editName.label')}
id='displayName'
onChange={(e) => this.setState({displayName: e.target.value})}
rows={3}/><br/>
<Button
onClick={this.onSubmitClick}>
{
lang.t('editName.button')
}
</Button>
</div> : null
}
</div>;
}
}
export default SuspendedAccount;
+12 -4
View File
@@ -4,6 +4,13 @@ export const CLEAN_STATE = 'CLEAN_STATE';
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
export const CREATE_DISPLAYNAME_REQUEST = 'CREATE_DISPLAYNAME_REQUEST';
export const CREATEDISPLAYNAME_SUCCESS = 'CREATEDISPLAYNAME_SUCCESS';
export const CREATEDISPLAYNAME_FAILURE = 'CREATEDISPLAYNAME_FAILURE';
export const CREATE_DISPLAYNAME = 'CREATE_DISPLAYNAME';
export const SHOW_CREATEDISPLAYNAME_DIALOG = 'SHOW_CREATEDISPLAYNAME_DIALOG';
export const HIDE_CREATEDISPLAYNAME_DIALOG = 'HIDE_CREATEDISPLAYNAME_DIALOG';
export const FETCH_SIGNUP_REQUEST = 'FETCH_SIGNUP_REQUEST';
export const FETCH_SIGNUP_FAILURE = 'FETCH_SIGNUP_FAILURE';
export const FETCH_SIGNUP_SUCCESS = 'FETCH_SIGNUP_SUCCESS';
@@ -16,6 +23,7 @@ export const FETCH_SIGNIN_FACEBOOK_REQUEST = 'FETCH_SIGNIN_FACEBOOK_REQUEST';
export const FETCH_SIGNIN_FACEBOOK_FAILURE = 'FETCH_SIGNIN_FACEBOOK_FAILURE';
export const FETCH_SIGNIN_FACEBOOK_SUCCESS = 'FETCH_SIGNIN_FACEBOOK_SUCCESS';
export const FETCH_SIGNUP_FACEBOOK_REQUEST = 'FETCH_SIGNUP_FACEBOOK_REQUEST';
export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE';
@@ -33,7 +41,7 @@ export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN';
export const EMAIL_CONFIRM_ERROR = 'EMAIL_CONFIRM_ERROR';
export const CONFIRM_EMAIL_REQUEST = 'CONFIRM_EMAIL_REQUEST';
export const CONFIRM_EMAIL_SUCCESS = 'CONFIRM_EMAIL_SUCCESS';
export const CONFIRM_EMAIL_FAILURE = 'CONFIRM_EMAIL_FAILURE';
export const VERIFY_EMAIL_REQUEST = 'VERIFY_EMAIL_REQUEST';
export const VERIFY_EMAIL_SUCCESS = 'VERIFY_EMAIL_SUCCESS';
export const VERIFY_EMAIL_FAILURE = 'VERIFY_EMAIL_FAILURE';
export const UPDATE_DISPLAYNAME = 'UPDATE_DISPLAYNAME';
+4 -3
View File
@@ -1,7 +1,8 @@
export const SAVE_BIO_REQUEST = 'SAVE_BIO_REQUEST';
export const SAVE_BIO_SUCCESS = 'SAVE_BIO_SUCCESS';
export const SAVE_BIO_FAILURE = 'SAVE_BIO_FAILURE';
export const EDIT_NAME_REQUEST = 'EDIT_NAME_REQUEST';
export const EDIT_NAME_SUCCESS = 'EDIT_NAME_SUCCESS';
export const EDIT_NAME_FAILURE = 'EDIT_NAME_FAILURE';
export const COMMENTS_BY_USER_REQUEST = 'COMMENTS_BY_USER_REQUEST';
export const COMMENTS_BY_USER_SUCCESS = 'COMMENTS_BY_USER_SUCCESS';
export const COMMENTS_BY_USER_FAILURE = 'COMMENTS_BY_USER_FAILURE';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const UPDATE_DISPLAYNAME = 'UPDATE_DISPLAYNAME';
@@ -6,9 +6,6 @@ fragment commentView on Comment {
user {
id
name: displayName
settings {
bio
}
}
actions {
type: action_type
@@ -11,6 +11,9 @@ function getQueryVariable(variable) {
return decodeURIComponent(pair[1]);
}
}
// If no query is included, return a default string for development
return 'http://dev.default.stream';
}
export const queryStream = graphql(STREAM_QUERY, {
+2 -1
View File
@@ -6,5 +6,6 @@ export default {
email: lang.t('error.email'),
password: lang.t('error.password'),
displayName: lang.t('error.displayName'),
confirmPassword: lang.t('error.confirmPassword')
confirmPassword: lang.t('error.confirmPassword'),
organizationName: lang.t('error.organizationName'),
};
+2 -1
View File
@@ -14,7 +14,8 @@ const buildOptions = (inputOptions = {}) => {
_csrf: csurfDOM ? csurfDOM.content : false
};
const options = Object.assign({}, defaultOptions, inputOptions);
let options = Object.assign({}, defaultOptions, inputOptions);
options.headers = Object.assign({}, defaultOptions.headers, inputOptions.headers);
if (options._csrf) {
switch (options.method.toLowerCase()) {
+2 -1
View File
@@ -2,5 +2,6 @@ export default {
email: email => (/^([A-Za-z0-9_\-\.\+])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
password: pass => (/^(?=.{8,}).*$/.test(pass)),
confirmPassword: () => true,
displayName: displayName => (/^[a-zA-Z0-9_]+$/.test(displayName))
displayName: displayName => (/^[a-zA-Z0-9_]+$/.test(displayName)),
organizationName: org => (/^[a-zA-Z0-9_ ]+$/).test(org)
};
+44 -17
View File
@@ -1,4 +1,4 @@
import {Map} from 'immutable';
import {Map, fromJS} from 'immutable';
import * as actions from '../constants/auth';
const initialState = Map({
@@ -7,19 +7,21 @@ const initialState = Map({
isAdmin: false,
user: null,
showSignInDialog: false,
showCreateDisplayNameDialog: false,
view: 'SIGNIN',
error: '',
passwordRequestSuccess: null,
passwordRequestFailure: null,
emailConfirmationFailure: false,
emailConfirmationLoading: false,
emailConfirmationSuccess: false,
successSignUp: false
emailVerificationFailure: false,
emailVerificationLoading: false,
emailVerificationSuccess: false,
successSignUp: false,
fromSignUp: false
});
const purge = user => {
const {settings, profiles, ...userData} = user; // eslint-disable-line
return userData;
return fromJS(userData);
};
export default function auth (state = initialState, action) {
@@ -36,11 +38,26 @@ export default function auth (state = initialState, action) {
error: '',
passwordRequestFailure: null,
passwordRequestSuccess: null,
emailConfirmationFailure: false,
emailConfirmationSuccess: false,
emailConfirmationLoading: false,
emailVerificationFailure: false,
emailVerificationSuccess: false,
emailVerificationLoading: false,
successSignUp: false
}));
case actions.SHOW_CREATEDISPLAYNAME_DIALOG :
return state
.set('showCreateDisplayNameDialog', true);
case actions.HIDE_CREATEDISPLAYNAME_DIALOG :
return state.merge(Map({
showCreateDisplayNameDialog: false
}));
case actions.CREATEDISPLAYNAME_SUCCESS :
return state.merge(Map({
showCreateDisplayNameDialog: false,
error: ''
}));
case actions.CREATEDISPLAYNAME_FAILURE :
return state
.set('error', action.error);
case actions.CHANGE_VIEW :
return state
.set('error', '')
@@ -72,6 +89,12 @@ export default function auth (state = initialState, action) {
.set('isLoading', false)
.set('error', action.error)
.set('user', null);
case actions.FETCH_SIGNUP_FACEBOOK_REQUEST:
return state
.set('fromSignUp', true);
case actions.FETCH_SIGNIN_FACEBOOK_REQUEST:
return state
.set('fromSignUp', false);
case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
return state
.set('user', purge(action.user))
@@ -107,16 +130,20 @@ export default function auth (state = initialState, action) {
return state
.set('passwordRequestFailure', 'There was an error sending your password reset email. Please try again soon!')
.set('passwordRequestSuccess', null);
case actions.EMAIL_CONFIRM_ERROR:
case actions.UPDATE_DISPLAYNAME:
console.log('Action', action);
return state
.set('emailConfirmationFailure', true)
.set('emailConfirmationLoading', false);
case actions.CONFIRM_EMAIL_REQUEST:
return state.set('emailConfirmationLoading', true);
case actions.CONFIRM_EMAIL_SUCCESS:
.setIn(['user', 'displayName'], action.displayName);
case actions.VERIFY_EMAIL_FAILURE:
return state
.set('emailConfirmationSuccess', true)
.set('emailConfirmationLoading', false);
.set('emailVerificationFailure', true)
.set('emailVerificationLoading', false);
case actions.VERIFY_EMAIL_REQUEST:
return state.set('emailVerificationLoading', true);
case actions.VERIFY_EMAIL_SUCCESS:
return state
.set('emailVerificationSuccess', true)
.set('emailVerificationLoading', false);
default :
return state;
}
+12 -3
View File
@@ -1,15 +1,22 @@
{
"en": {
"successUpdateSettings": "The changes you have made have been applied to the comment stream on this article",
"successBioUpdate": "Your Bio has been updated",
"successNameUpdate": "Your display name has been updated",
"contentNotAvailable": "This content is not available",
"suspendedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information",
"bannedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information",
"editName": {
"msg": "Your account is currently suspended because your display name has been deemed inappropriate. To restore your account, please enter a new username. You may contact moderator@fakeurl.com for more information.",
"label": "New Display Name",
"button": "Submit",
"error": "Display names can contain letters, numbers and _ only"
},
"error": {
"emailNotVerified": "Email address {0} not verified.",
"email": "Not a valid E-Mail",
"password": "Password must be at least 8 characters",
"displayName": "Display names can contain letters, numbers and _ only",
"confirmPassword": "Passwords don't match. Please, check again",
"organizationName": "Organization name must only contain letters or numbers.",
"emailPasswordError": "Email and/or password combination incorrect.",
"EMAIL_REQUIRED": "An email address is required",
"PASSWORD_REQUIRED": "Must input a password",
@@ -26,12 +33,14 @@
"successUpdateSettings": "La configuración de este articulo fue actualizada",
"successBioUpdate": "Tu bio fue actualizada",
"contentNotAvailable": "El contenido no se encuentra disponible",
"suspendedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information",
"bannedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information",
"editNameMsg": "",
"error": {
"emailNotVerified": "Dirección de correo electrónico {0} no verificada.",
"email": "No es un email válido",
"password": "La contraseña debe tener por lo menos 8 caracteres",
"displayName": "Los nombres pueden contener letras, números y _",
"organizationName": "El nombre de la organización debe contener letras y/o números.",
"confirmPassword": "Las contraseñas no coinciden",
"emailPasswordError": "Email y/o contraseña incorrecta.",
"EMAIL_REQUIRED": "Se requiere una dirección de correo electrónico",
+4 -19
View File
@@ -1,6 +1,4 @@
import React, {Component} from 'react';
import {Tooltip} from 'coral-ui';
import FlagBio from 'coral-plugin-flags/FlagBio';
const packagename = 'coral-plugin-author-name';
import styles from './styles.css';
@@ -24,24 +22,11 @@ export default class AuthorName extends Component {
render () {
const {author} = this.props;
const {showTooltip} = this.state;
return (
<div className={`${packagename}-text ${styles.container}`} onClick={this.handleClick} onMouseLeave={this.handleMouseLeave}>
<a className={`${styles.authorName} ${author.settings.bio ? styles.hasBio : ''}`}>
{author && author.name}
{author.settings.bio ? <span className={`${styles.arrowDown} ${showTooltip ? styles.arrowUp : ''}`} /> : null}
</a>
{showTooltip && author.settings.bio
&& (
<Tooltip>
<div className={`${packagename}-bio`}>
{author.settings.bio}
</div>
<div className={`${packagename}-bio-flag`}>
<FlagBio {...this.props}/>
</div>
</Tooltip>
)}
<div
className={`${packagename}-text`}
className={`${styles.authorName}`}>
{author && author.name}
</div>
);
}
@@ -15,7 +15,6 @@ export default ({showSignInDialog}) => (
From the Settings Page you can
<ul>
<li>See your comment history</li>
<li>Write a bio about yourself to display to the community</li>
</ul>
</div>
</div>
@@ -4,12 +4,10 @@ import React, {Component} from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import {myCommentHistory} from 'coral-framework/graphql/queries';
import {saveBio} from 'coral-framework/actions/user';
import BioContainer from './BioContainer';
import {link} from 'coral-framework/services/PymConnection';
import NotLoggedIn from '../components/NotLoggedIn';
import {TabBar, Tab, TabContent, Spinner} from 'coral-ui';
import {Spinner} from 'coral-ui';
import SettingsHeader from '../components/SettingsHeader';
import CommentHistory from 'coral-plugin-history/CommentHistory';
@@ -33,8 +31,7 @@ class SettingsContainer extends Component {
}
render() {
const {loggedIn, userData, asset, showSignInDialog, data, user} = this.props;
const {activeTab} = this.state;
const {loggedIn, asset, showSignInDialog, data} = this.props;
const {me} = this.props.data;
if (!loggedIn || !me) {
@@ -48,25 +45,30 @@ class SettingsContainer extends Component {
return (
<div>
<SettingsHeader {...this.props} />
<TabBar onChange={this.handleTabChange} activeTab={activeTab} cStyle='material'>
<Tab>{lang.t('allComments')} ({user.myComments.length})</Tab>
<Tab>{lang.t('profileSettings')}</Tab>
</TabBar>
<TabContent show={activeTab === 0}>
{
me.comments.length ?
<CommentHistory
comments={me.comments}
asset={asset}
link={link}
/>
:
<p>{lang.t('userNoComment')}</p>
}
</TabContent>
<TabContent show={activeTab === 1}>
<BioContainer bio={userData.settings.bio} handleSave={this.handleSave} {...this.props} />
</TabContent>
{
// Hiding bio until moderation can get figured out
/* <TabBar onChange={this.handleTabChange} activeTab={activeTab} cStyle='material'>
<Tab>{lang.t('allComments')} ({user.myComments.length})</Tab>
<Tab>{lang.t('profileSettings')}</Tab>
</TabBar>
<TabContent show={activeTab === 0}> */
me.comments.length ?
<CommentHistory
comments={me.comments}
asset={asset}
link={link}
/>
:
<p>{lang.t('userNoComment')}</p>
// Hiding user bio pending effective moderation system.
/* </TabContent>
<TabContent show={activeTab === 1}>
<BioContainer bio={userData.settings.bio} handleSave={this.handleSave} {...this.props} />
</TabContent> */
}
</div>
);
}
@@ -78,8 +80,9 @@ const mapStateToProps = state => ({
auth: state.auth.toJS()
});
const mapDispatchToProps = dispatch => ({
saveBio: (user_id, formData) => dispatch(saveBio(user_id, formData))
const mapDispatchToProps = () => ({
// saveBio: (user_id, formData) => dispatch(saveBio(user_id, formData))
});
export default compose(
@@ -0,0 +1,48 @@
import React from 'react';
import FormField from 'coral-ui/components/FormField';
import Alert from './Alert';
import Button from 'coral-ui/components/Button';
import {Dialog} from 'coral-ui';
import styles from './styles.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
const CreateDisplayNameDialog = ({open, handleClose, offset, formData, handleSubmitDisplayName, handleChange, ...props}) => (
<Dialog
className={styles.dialog}
id="createDisplayNameDialog"
open={open}
style={{
position: 'relative',
top: offset !== 0 && offset
}}>
<span className={styles.close} onClick={handleClose}>×</span>
<div>
<div className={styles.header}>
<h1>
{lang.t('createdisplay.writeyourusername')}
</h1>
</div>
<div>
<label htmlFor="displayName">{lang.t('createdisplay.yourusername')}</label>
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
<form id="saveDisplayName" onSubmit={handleSubmitDisplayName}>
<FormField
id="displayName"
type="string"
label={lang.t('createdisplay.displayName')}
value={formData.displayName}
onChange={handleChange}
/>
{ props.errors.displayName && <span className={styles.hint}> {lang.t('createdisplay.specialCharacters')} </span> }
<div className={styles.action}>
<Button id="save" type="submit" className={styles.saveButton}>{lang.t('createdisplay.save')}</Button>
</div>
</form>
</div>
</div>
</Dialog>
);
export default CreateDisplayNameDialog;
@@ -17,12 +17,7 @@ const SignDialog = ({open, view, handleClose, offset, ...props}) => (
}}>
<span className={styles.close} onClick={handleClose}>×</span>
{view === 'SIGNIN' && <SignInContent {...props} />}
{
view === 'SIGNUP' && <SignUpContent
emailConfirmationLoading={props.emailConfirmationLoading}
emailConfirmationSuccess={props.emailConfirmationSuccess}
{...props} />
}
{view === 'SIGNUP' && <SignUpContent {...props} />}
{view === 'FORGOT' && <ForgotContent {...props} />}
</Dialog>
);
@@ -10,35 +10,28 @@ const SignInContent = ({
handleChange,
handleChangeEmail,
emailToBeResent,
handleResendConfirmation,
emailConfirmationLoading,
emailConfirmationSuccess,
handleResendVerification,
emailVerificationLoading,
emailVerificationSuccess,
formData,
...props
changeView,
handleSignIn,
auth,
fetchSignInFacebook
}) => {
return (
<div>
<div className={styles.header}>
<h1>
{props.auth.emailConfirmationFailure ? lang.t('signIn.emailConfirmCTA') : lang.t('signIn.signIn')}
{auth.emailVerificationFailure ? lang.t('signIn.emailVerifyCTA') : lang.t('signIn.signIn')}
</h1>
</div>
<div className={styles.socialConnections}>
<Button cStyle="facebook" onClick={props.fetchSignInFacebook} full>
{lang.t('signIn.facebookSignIn')}
</Button>
</div>
<div className={styles.separator}>
<h1>
{lang.t('signIn.or')}
</h1>
</div>
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
{ auth.error && <Alert>{auth.error}</Alert> }
{
props.auth.emailConfirmationFailure
? <form onSubmit={handleResendConfirmation}>
<p>{lang.t('signIn.requestNewConfirmEmail')}</p>
auth.emailVerificationFailure
? <form onSubmit={handleResendVerification}>
<p>{lang.t('signIn.requestNewVerifyEmail')}</p>
<FormField
id="confirm-email"
type="email"
@@ -46,41 +39,53 @@ const SignInContent = ({
value={emailToBeResent}
onChange={handleChangeEmail} />
<Button id='resendConfirmEmail' type='submit' cStyle='black' full>Send Email</Button>
{emailConfirmationLoading && <Spinner />}
{emailConfirmationSuccess && <Success />}
{emailVerificationLoading && <Spinner />}
{emailVerificationSuccess && <Success />}
</form>
: <form onSubmit={props.handleSignIn}>
<FormField
id="email"
type="email"
label={lang.t('signIn.email')}
value={formData.email}
onChange={handleChange}
/>
<FormField
id="password"
type="password"
label={lang.t('signIn.password')}
value={formData.password}
onChange={handleChange}
/>
<div className={styles.action}>
{
!props.auth.isLoading ?
<Button id='coralLogInButton' type="submit" cStyle="black" className={styles.signInButton} full>
{lang.t('signIn.signIn')}
</Button>
:
<Spinner />
}
: <div>
<div className={styles.socialConnections}>
<Button cStyle="facebook" onClick={fetchSignInFacebook} full>
{lang.t('signIn.facebookSignIn')}
</Button>
</div>
</form>
<div className={styles.separator}>
<h1>
{lang.t('signIn.or')}
</h1>
</div>
<form onSubmit={handleSignIn}>
<FormField
id="email"
type="email"
label={lang.t('signIn.email')}
value={formData.email}
onChange={handleChange}
/>
<FormField
id="password"
type="password"
label={lang.t('signIn.password')}
value={formData.password}
onChange={handleChange}
/>
<div className={styles.action}>
{
!auth.isLoading ?
<Button id='coralLogInButton' type="submit" cStyle="black" className={styles.signInButton} full>
{lang.t('signIn.signIn')}
</Button>
:
<Spinner />
}
</div>
</form>
</div>
}
<div className={styles.footer}>
<span><a onClick={() => props.changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')}</a></span>
<span><a onClick={() => changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')}</a></span>
<span>
{lang.t('signIn.needAnAccount')}
<a onClick={() => props.changeView('SIGNUP')} id='coralRegister'>
<a onClick={() => changeView('SIGNUP')} id='coralRegister'>
{lang.t('signIn.register')}
</a>
</span>
@@ -90,9 +95,17 @@ const SignInContent = ({
};
SignInContent.propTypes = {
emailConfirmationLoading: PropTypes.bool.isRequired,
emailConfirmationSuccess: PropTypes.bool.isRequired,
handleResendConfirmation: PropTypes.func.isRequired,
auth: PropTypes.shape({
isLoading: PropTypes.bool.isRequired,
error: PropTypes.string,
emailVerificationFailure: PropTypes.bool
}).isRequired,
fetchSignInFacebook: PropTypes.func.isRequired,
handleSignIn: PropTypes.func.isRequired,
changeView: PropTypes.func.isRequired,
emailVerificationLoading: PropTypes.bool.isRequired,
emailVerificationSuccess: PropTypes.bool.isRequired,
handleResendVerification: PropTypes.func.isRequired,
handleChangeEmail: PropTypes.func.isRequired,
emailToBeResent: PropTypes.string.isRequired
};
+142 -77
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, {PropTypes} from 'react';
import Alert from './Alert';
import {Button, FormField, Spinner, Success} from 'coral-ui';
import styles from './styles.css';
@@ -6,83 +6,148 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
const SignUpContent = ({handleChange, formData, ...props}) => (
<div>
<div className={styles.header}>
<h1>
{lang.t('signIn.signUp')}
</h1>
</div>
<div className={styles.socialConnections}>
<Button cStyle="facebook" onClick={props.fetchSignInFacebook} full>
{lang.t('signIn.facebookSignUp')}
</Button>
</div>
<div className={styles.separator}>
<h1>
{lang.t('signIn.or')}
</h1>
</div>
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
<form onSubmit={props.handleSignUp}>
<FormField
id="email"
type="email"
label={lang.t('signIn.email')}
value={formData.email}
showErrors={props.showErrors}
errorMsg={props.errors.email}
onChange={handleChange}
/>
<FormField
id="displayName"
type="text"
label={lang.t('signIn.displayName')}
value={formData.displayName}
showErrors={props.showErrors}
errorMsg={props.errors.displayName}
onChange={handleChange}
/>
<FormField
id="password"
type="password"
label={lang.t('signIn.password')}
value={formData.password}
showErrors={props.showErrors}
errorMsg={props.errors.password}
onChange={handleChange}
minLength="8"
/>
{ props.errors.password && <span className={styles.hint}> Password must be at least 8 characters. </span> }
<FormField
id="confirmPassword"
type="password"
label={lang.t('signIn.confirmPassword')}
value={formData.confirmPassword}
showErrors={props.showErrors}
errorMsg={props.errors.confirmPassword}
onChange={handleChange}
minLength="8"
/>
<div className={styles.action}>
{ !props.auth.isLoading && !props.auth.successSignUp && (
<Button type="submit" cStyle="black" id='coralSignUpButton' className={styles.signInButton} full>
class SignUpContent extends React.Component {
constructor (props) {
super(props);
this.successfulSignup = false;
}
static propTypes = {
emailVerificationEnabled: PropTypes.bool.isRequired,
fetchSignUpFacebook: PropTypes.func.isRequired,
changeView: PropTypes.func.isRequired,
handleSignUp: PropTypes.func.isRequired,
showErrors: PropTypes.bool,
errors: PropTypes.shape({
email: PropTypes.string,
displayName: PropTypes.string,
password: PropTypes.string,
confirmPassword: PropTypes.string,
}),
formData: PropTypes.shape({
email: PropTypes.string,
displayName: PropTypes.string,
password: PropTypes.string,
confirmPassword: PropTypes.string
})
}
render () {
const {
handleChange,
formData,
emailVerificationEnabled,
auth,
errors,
showErrors,
changeView,
handleSignUp,
fetchSignUpFacebook} = this.props;
const beforeSignup = !auth.isLoading && !auth.successSignUp;
const successfulSignup = !auth.isLoading && auth.successSignUp;
// the first time we render a successfulSignup, trigger a timer
if ((this.successfulSignup ^ successfulSignup) && !emailVerificationEnabled) {
setTimeout(() => {
changeView('SIGNIN');
}, 1000);
this.successfulSignup = true;
}
return (
<div>
<div className={styles.header}>
<h1>
{lang.t('signIn.signUp')}
</Button>
)}
{ props.auth.isLoading && <Spinner /> }
{ !props.auth.isLoading && props.auth.successSignUp && <Success /> }
</h1>
</div>
{ auth.error && <Alert>{auth.error}</Alert> }
{ beforeSignup &&
<div>
<div className={styles.socialConnections}>
<Button cStyle="facebook" onClick={fetchSignUpFacebook} full>
{lang.t('signIn.facebookSignUp')}
</Button>
</div>
<div className={styles.separator}>
<h1>
{lang.t('signIn.or')}
</h1>
</div>
<form onSubmit={handleSignUp}>
<FormField
id="email"
type="email"
label={lang.t('signIn.email')}
value={formData.email}
showErrors={showErrors}
errorMsg={errors.email}
onChange={handleChange}
/>
<FormField
id="displayName"
type="text"
label={lang.t('signIn.displayName')}
value={formData.displayName}
showErrors={showErrors}
errorMsg={errors.displayName}
onChange={handleChange}
/>
<FormField
id="password"
type="password"
label={lang.t('signIn.password')}
value={formData.password}
showErrors={showErrors}
errorMsg={errors.password}
onChange={handleChange}
minLength="8"
/>
{ errors.password && <span className={styles.hint}> Password must be at least 8 characters. </span> }
<FormField
id="confirmPassword"
type="password"
label={lang.t('signIn.confirmPassword')}
value={formData.confirmPassword}
showErrors={showErrors}
errorMsg={errors.confirmPassword}
onChange={handleChange}
minLength="8"
/>
<div className={styles.action}>
<Button type="submit" cStyle="black" id='coralSignUpButton' className={styles.signInButton} full>
{lang.t('signIn.signUp')}
</Button>
{ auth.isLoading && <Spinner /> }
</div>
</form>
</div>
}
{
successfulSignup &&
<div>
<Success />
{
emailVerificationEnabled &&
<p>{lang.t('signIn.verifyEmail')}<br /><br />{lang.t('signIn.verifyEmail2')}</p>
}
</div>
}
<div className={styles.footer}>
<span>
{lang.t('signIn.alreadyHaveAnAccount')}
<a id="coralSignInViewTrigger" onClick={() => changeView('SIGNIN')}>
{lang.t('signIn.signIn')}
</a>
</span>
</div>
</div>
</form>
<div className={styles.footer}>
<span>
{lang.t('signIn.alreadyHaveAnAccount')}
<a onClick={() => props.changeView('SIGNIN')}>
{lang.t('signIn.signIn')}
</a>
</span>
</div>
</div>
);
);
}
}
export default SignUpContent;
@@ -0,0 +1,135 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
import CreateDisplayNameDialog from '../components/CreateDisplayNameDialog';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
import {
showCreateDisplayNameDialog,
hideCreateDisplayNameDialog,
invalidForm,
validForm,
createDisplayName
} from '../../coral-framework/actions/auth';
class ChangeDisplayNameContainer extends Component {
initialState = {
formData: {
displayName: '',
},
errors: {},
showErrors: false
};
constructor(props) {
super(props);
this.state = this.initialState;
this.handleChange = this.handleChange.bind(this);
this.handleSubmitDisplayName = this.handleSubmitDisplayName.bind(this);
this.handleClose = this.handleClose.bind(this);
this.addError = this.addError.bind(this);
}
handleChange(e) {
const {name, value} = e.target;
this.setState(state => ({
...state,
formData: {
...state.formData,
[name]: value
}
}), () => {
this.validation(name, value);
});
}
addError(name, error) {
return this.setState(state => ({
errors: {
...state.errors,
[name]: error
}
}));
}
validation(name, value) {
const {addError} = this;
if (!value.length) {
addError(name, lang.t('createdisplay.requiredField'));
} else if (!validate[name](value)) {
addError(name, errorMsj[name]);
} else {
const { [name]: prop, ...errors } = this.state.errors; // eslint-disable-line
// Removes Error
this.setState(state => ({...state, errors}));
}
}
isCompleted() {
const {formData} = this.state;
return !Object.keys(formData).filter(prop => !formData[prop].length).length;
}
displayErrors(show = true) {
this.setState({showErrors: show});
}
handleSubmitDisplayName(e) {
e.preventDefault();
const {errors} = this.state;
const {validForm, invalidForm} = this.props;
this.displayErrors();
if (this.isCompleted() && !Object.keys(errors).length) {
this.props.createDisplayName(this.props.user.id, this.state.formData);
validForm();
} else {
invalidForm(lang.t('createdisplay.checkTheForm'));
}
}
handleClose() {
this.props.hideCreateDisplayNameDialog();
}
render() {
const {loggedIn, auth, offset} = this.props;
return (
<div>
<CreateDisplayNameDialog
open={auth.showCreateDisplayNameDialog && auth.fromSignUp}
offset={offset}
handleClose={this.handleClose}
loggedIn={loggedIn}
handleSubmitDisplayName={this.handleSubmitDisplayName}
{...this}
{...this.state}
{...this.props}
/>
</div>
);
}
}
const mapStateToProps = state => ({
auth: state.auth.toJS()
});
const mapDispatchToProps = dispatch => ({
createDisplayName: (userid, formData) => dispatch(createDisplayName(userid, formData)),
showCreateDisplayNameDialog: () => dispatch(showCreateDisplayNameDialog()),
hideCreateDisplayNameDialog: () => dispatch(hideCreateDisplayNameDialog()),
invalidForm: error => dispatch(invalidForm(error)),
validForm: () => dispatch(validForm())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ChangeDisplayNameContainer);
@@ -1,4 +1,4 @@
import React, {Component} from 'react';
import React, {Component, PropTypes} from 'react';
import {connect} from 'react-redux';
import SignDialog from '../components/SignDialog';
import Button from 'coral-ui/components/Button';
@@ -6,6 +6,7 @@ import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
import {pym} from 'coral-framework';
const lang = new I18n(translations);
import {
@@ -15,6 +16,7 @@ import {
showSignInDialog,
hideSignInDialog,
fetchSignInFacebook,
fetchSignUpFacebook,
fetchForgotPassword,
requestConfirmEmail,
facebookCallback,
@@ -41,12 +43,16 @@ class SignInContainer extends Component {
this.state = this.initialState;
this.handleChange = this.handleChange.bind(this);
this.handleChangeEmail = this.handleChangeEmail.bind(this);
this.handleResendConfirmation = this.handleResendConfirmation.bind(this);
this.handleResendVerification = this.handleResendVerification.bind(this);
this.handleSignUp = this.handleSignUp.bind(this);
this.handleSignIn = this.handleSignIn.bind(this);
this.addError = this.addError.bind(this);
}
static propTypes = {
requireEmailConfirmation: PropTypes.bool.isRequired
}
componentWillMount () {
this.props.checkLogin();
}
@@ -79,9 +85,9 @@ class SignInContainer extends Component {
this.setState({emailToBeResent: value});
}
handleResendConfirmation(e) {
handleResendVerification(e) {
e.preventDefault();
this.props.requestConfirmEmail(this.state.emailToBeResent)
this.props.requestConfirmEmail(this.state.emailToBeResent, pym.parentUrl || location.href)
.then(() => {
setTimeout(() => {
@@ -132,7 +138,7 @@ class SignInContainer extends Component {
const {fetchSignUp, validForm, invalidForm} = this.props;
this.displayErrors();
if (this.isCompleted() && !Object.keys(errors).length) {
fetchSignUp(this.state.formData);
fetchSignUp(this.state.formData, pym.parentUrl || location.href);
validForm();
} else {
invalidForm(lang.t('signIn.checkTheForm'));
@@ -145,8 +151,9 @@ class SignInContainer extends Component {
}
render() {
const {auth, showSignInDialog, noButton, offset} = this.props;
const {emailConfirmationLoading, emailConfirmationSuccess} = auth;
const {auth, showSignInDialog, noButton, offset, requireEmailConfirmation} = this.props;
const {emailVerificationLoading, emailVerificationSuccess} = auth;
return (
<div>
{!noButton && <Button id='coralSignInButton' onClick={showSignInDialog} full>
@@ -156,8 +163,9 @@ class SignInContainer extends Component {
open={auth.showSignInDialog}
view={auth.view}
offset={offset}
emailConfirmationLoading={emailConfirmationLoading}
emailConfirmationSuccess={emailConfirmationSuccess}
emailVerificationEnabled={requireEmailConfirmation}
emailVerificationLoading={emailVerificationLoading}
emailVerificationSuccess={emailVerificationSuccess}
{...this}
{...this.state}
{...this.props}
@@ -174,11 +182,12 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
checkLogin: () => dispatch(checkLogin()),
facebookCallback: (err, data) => dispatch(facebookCallback(err, data)),
fetchSignUp: formData => dispatch(fetchSignUp(formData)),
fetchSignUp: (formData, url) => dispatch(fetchSignUp(formData, url)),
fetchSignIn: formData => dispatch(fetchSignIn(formData)),
fetchSignInFacebook: () => dispatch(fetchSignInFacebook()),
fetchSignUpFacebook: () => dispatch(fetchSignUpFacebook()),
fetchForgotPassword: formData => dispatch(fetchForgotPassword(formData)),
requestConfirmEmail: email => dispatch(requestConfirmEmail(email)),
requestConfirmEmail: (email, url) => dispatch(requestConfirmEmail(email, url)),
showSignInDialog: () => dispatch(showSignInDialog()),
changeView: view => dispatch(changeView(view)),
handleClose: () => dispatch(hideSignInDialog()),
+30 -6
View File
@@ -1,8 +1,10 @@
export default {
en: {
'signIn': {
emailConfirmCTA: 'Please verify your email address.',
requestNewConfirmEmail: 'Request another email:',
emailVerifyCTA: 'Please verify your email address.',
requestNewVerifyEmail: 'Request another email:',
verifyEmail: 'Thank you for creating an account! We sent an email to the address you provided to verify your account.',
verifyEmail2: 'You must verify your account before engaging with the community.',
notYou: 'Not you?',
loggedInAs: 'Logged in as',
facebookSignIn: 'Sign in with Facebook',
@@ -26,12 +28,24 @@ export default {
passwordsDontMatch: 'Passwords don\'t match.',
specialCharacters: 'Display names can contain letters, numbers and _ only',
checkTheForm: 'Invalid Form. Please, check the fields'
}
},
'createdisplay': {
writeyourusername: 'Write your username',
yourusername: 'Your username is publicly visible on all comments you post. A username is needed before you can post your first comment.',
displayName: 'Display Name',
save: 'Save',
requiredField: 'Required field',
errorCreate: 'Error when changing display name',
checkTheForm: 'Invalid Form. Please, check the fields',
specialCharacters: 'Display names can contain letters, numbers and _ only'
},
},
es: {
'signIn': {
emailConfirmCTA: 'Por favor verifique su correo electronico.',
requestNewConfirmEmail: 'Enviar otro correo:',
emailVerifyCTA: 'Por favor verifique su correo electronico.',
requestNewVerifyEmail: 'Enviar otro correo:',
verifyEmail: '¡Gracias por crear una cuenta! Le enviamos un correo a la dirección que dio para verificar su cuenta.',
verifyEmail2: 'Debe verificarla antes de poder involucrarse en la comunidad.',
notYou: 'No eres tu?',
loggedInAs: 'Entraste como',
facebookSignIn: 'Entrar con Facebook',
@@ -55,6 +69,16 @@ export default {
passwordsDontMatch: 'Las contraseñas no coinciden',
specialCharacters: 'Los nombres pueden contener letras, números y _',
checkTheForm: 'Formulario Inválido. Por favor, completa los campos'
}
},
'createdisplay': {
writeyourusername: 'Escribe tu nombre',
yourusername: 'Tu nombre es visible publicamente en todos los comentarios que publiques. Es necesario tener un nombre de usuario antes de poder publicar tu primer comentario.',
displayName: 'Nombre a mostrar',
save: 'Guardar',
requiredField: 'Campo necesario',
errorCreate: 'Hubo un error al cambiar el nombre de usuario',
checkTheForm: 'Formulario Invalido. Por favor, verifica los campos',
specialCharacters: 'Sólo pueden contener letras, números y _'
},
}
};
+6
View File
@@ -30,6 +30,12 @@
font-size: 18px;
vertical-align: middle;
}
&:disabled {
background: #E0E0E0;
color: #4f5c67;
font-weight: bold;
}
}
.type--black {
+3
View File
@@ -0,0 +1,3 @@
.Option {
}
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
import {Option as OptionMDL} from 'react-mdl-selectfield';
import styles from './Option.css';
const Option = props => {
const {children, ...attrs} = props;
return (
<OptionMDL className={styles.Option} {...attrs}>
{children}
</OptionMDL>
);
};
export default Option;
+33
View File
@@ -0,0 +1,33 @@
.Select {
position: relative;
width: 100%;
height: 40px;
background: #2c2c2c;
padding: 10px 15px;
box-sizing: border-box;
color: white;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
> div {
padding: 0;
}
i {
position: absolute;
top: 7px;
right: 7px;
}
input {
padding: 0;
font-size: 13px;
letter-spacing: 0.7px;
font-weight: 400;
}
&:hover {
cursor: pointer;
}
}
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
import {SelectField} from 'react-mdl-selectfield';
import styles from './Select.css';
const Select = props => {
const {children, ...attrs} = props;
return (
<SelectField className={styles.Select} {...attrs}>
{children}
</SelectField>
);
};
export default Select;
+27
View File
@@ -0,0 +1,27 @@
import React, {PropTypes} from 'react';
const Wizard = (props) => {
const {children, currentStep, ...rest} = props;
return (
<section>
{React.Children.toArray(children)
.filter((child, i) => i === currentStep)
.map((child, i) =>
React.cloneElement(child, {
i,
currentStep,
...rest
})
)}
</section>
);
};
Wizard.propTypes = {
currentStep: PropTypes.number.isRequired,
nextStep: PropTypes.func.isRequired,
previousStep: PropTypes.func.isRequired,
goToStep: PropTypes.func.isRequired
};
export default Wizard;
+107
View File
@@ -0,0 +1,107 @@
.WizardNav {
ul {
list-style: none;
}
li {
border: solid 1px #DFDFDF;
background-color: #F0F0F0;
display: inline-block;
padding: 10px;
margin-right: 10px;
color: #A7A7A7;
position: relative;
padding-left: 40px;
border-radius: 1px;
&:hover {
cursor: pointer;
}
&.done {
border-color: #00796B;
background-color: #00796B;
color: #ffffff;
span {
&::after {
border-color: transparent transparent transparent #00796B;
}
}
i {
opacity: 1;
}
}
&.active {
background-color: #FFFFFF;
color: #636363;
font-weight: 500;
span {
&::after {
border-color: transparent transparent transparent #FFFFFF;
}
}
}
i {
transition: opacity 0.2s ease;
opacity: 0;
vertical-align: middle;
font-size: 16px;
margin-top: -5px;
margin-left: 8px;
}
span {
padding: 10px 20px;
margin-right: 10px;
position: absolute;
top: -1px;
right: -51px;
z-index: 10;
&::before {
content: "";
display: inline-block;
position: absolute;
border: 23px solid #DFDFDF;
border-color: transparent transparent transparent #DFDFDF;
top: 0;
left: 0;
}
&::after {
content: "";
display: inline-block;
position: absolute;
border: 23px solid white;
border-color: transparent transparent transparent #f0f0f0;
top: 0;
left: -1px;
}
}
&::before {
content: "";
display: inline-block;
position: absolute;
border: 23px solid #DFDFDF;
border-color: transparent transparent transparent #DFDFDF;
top: -1px;
left: 0;
}
&::after {
content: "";
display: inline-block;
position: absolute;
border: 23px solid white;
border-color: transparent transparent transparent #fafafa;
top: -1px;
left: -1px;
}
}
}
+31
View File
@@ -0,0 +1,31 @@
import React, {PropTypes} from 'react';
import styles from './WizardNav.css';
import Icon from './Icon';
const WizardNav = props => {
const {goToStep = () => {}, currentStep, items, icon} = props;
return (
<nav className={styles.WizardNav}>
<ul>
{
items.map((item, i) => (
<li
key={i}
className={`${currentStep === item.step ? styles.active : ''} ${item.step < currentStep ? styles.done : ''}`}
onClick={() => goToStep(item.step)}>
{item.text}
{icon && <Icon name={icon} />}
<span/>
</li>
))
}
</ul>
</nav>
);
};
WizardNav.propTypes = {
currentStep: PropTypes.number.isRequired
};
export default WizardNav;
+4
View File
@@ -16,4 +16,8 @@ export {default as Card} from './components/Card';
export {default as FormField} from './components/FormField';
export {default as Success} from './components/Success';
export {default as Pager} from './components/Pager';
export {default as Wizard} from './components/Wizard';
export {default as WizardNav} from './components/WizardNav';
export {default as Select} from './components/Select';
export {default as Option} from './components/Option';
export {default as SnackBar} from './components/SnackBar';
+25
View File
@@ -0,0 +1,25 @@
version: '2'
services:
talk:
image: coralproject/talk:latest
restart: always
ports:
- "5000:5000"
depends_on:
- mongo
- redis
mongo:
image: mongo:3.2
restart: always
volumes:
- mongo:/data/db
redis:
image: redis:3.2
restart: always
volumes:
- redis:/data
volumes:
mongo:
external: false
redis:
external: false
+15 -1
View File
@@ -128,6 +128,18 @@ const ErrNotAuthorized = new APIError('not authorized', {
// initialized.
const ErrSettingsNotInit = new Error('settings not initialized, run `./bin/cli setup` to setup the application first');
// ErrSettingsInit is returned when the setup endpoint is hit and we are already
// initialized.
const ErrSettingsInit = new APIError('settings are already initialized', {
status: 500
});
// ErrInstallLock is returned when the setup endpoint is hit and the install
// lock is present.
const ErrInstallLock = new APIError('install lock active', {
status: 500
});
module.exports = {
ExtendableError,
APIError,
@@ -145,5 +157,7 @@ module.exports = {
ErrNotFound,
ErrInvalidAssetURL,
ErrAuthentication,
ErrNotAuthorized
ErrNotAuthorized,
ErrSettingsInit,
ErrInstallLock
};
-2
View File
@@ -2,7 +2,6 @@ const _ = require('lodash');
const Comment = require('./comment');
const Action = require('./action');
const User = require('./user');
module.exports = (context) => {
@@ -10,7 +9,6 @@ module.exports = (context) => {
return _.merge(...[
Comment,
Action,
User,
].map((mutators) => {
// Each set of mutators is a function which takes the context.
-31
View File
@@ -1,31 +0,0 @@
const UsersService = require('../../services/users');
/**
* Updates a users settings.
* @param {Object} user the user performing the request
* @param {String} bio the new user bio
* @return {Promise}
*/
const updateUserSettings = ({user}, {bio}) => {
return UsersService.updateSettings(user.id, {bio});
};
module.exports = (context) => {
// TODO: refactor to something that'll return an error in the event an attempt
// is made to mutate state while not logged in. There's got to be a better way
// to do this.
if (context.user && context.user.can('mutation:updateUserSettings')) {
return {
User: {
updateSettings: (settings) => updateUserSettings(context, settings)
}
};
}
return {
User: {
updateSettings: () => {}
}
};
};
-3
View File
@@ -8,9 +8,6 @@ const RootMutation = {
deleteAction(_, {id}, {mutators: {Action}}) {
return Action.delete({id});
},
updateUserSettings(_, {settings}, {mutators: {User}}) {
return User.updateSettings(settings);
}
};
module.exports = RootMutation;
+4 -16
View File
@@ -10,11 +10,6 @@ enum SORT_ORDER {
# Date represented as an ISO8601 string.
scalar Date
type UserSettings {
# bio of the user.
bio: String
}
input CommentsQuery {
# current status of a comment.
statuses: [COMMENT_STATUS]
@@ -22,7 +17,7 @@ input CommentsQuery {
# asset that a comment is on.
asset_id: ID
# the parent of the comment that we want to retrive.
# the parent of the comment that we want to retrieve.
parent_id: ID
# comments returned will only be ones which have at least one action of this
@@ -61,8 +56,8 @@ type User {
# the current roles of the user.
roles: [USER_ROLES]
# settings for a user.
settings: UserSettings
# determines whether the user can edit their username
canEditName: Boolean
# returns all comments based on a query.
comments(query: CommentsQuery): [Comment]
@@ -150,7 +145,7 @@ type Asset {
# The scraped title of the asset.
title: String
# The URL that the asset is locaed on.
# The URL that the asset is located on.
url: String
# The top level comments that are attached to the asset.
@@ -205,11 +200,6 @@ input CreateActionInput {
item_id: ID!
}
input UpdateUserSettingsInput {
# user bio
bio: String!
}
type RootMutation {
# creates a comment on the asset.
createComment(asset_id: ID!, parent_id: ID, body: String!): Comment
@@ -220,8 +210,6 @@ type RootMutation {
# delete an action based on the action id.
deleteAction(id: ID!): Boolean
# updates a user's settings, it will return if the query was successful.
updateUserSettings(settings: UpdateUserSettingsInput!): Boolean
}
schema {
+8 -2
View File
@@ -12,7 +12,7 @@ const USER_STATUS = [
'ACTIVE',
'BANNED',
'PENDING',
'SUSPENDED',
'APPROVED' // Indicates that the users' displayname has been approved
];
// ProfileSchema is the mongoose schema defined as the representation of a
@@ -97,6 +97,12 @@ const UserSchema = new mongoose.Schema({
default: 'ACTIVE'
},
// Determines whether the user can edit their username.
canEditName: {
type: Boolean,
default: false
},
// User's settings
settings: {
bio: {
@@ -141,7 +147,7 @@ const USER_GRAPH_OPERATIONS = [
'mutation:createComment',
'mutation:createAction',
'mutation:deleteAction',
'mutation:updateUserSettings'
'mutation:editName'
];
/**
+9 -10
View File
@@ -16,7 +16,7 @@ router.get('/', authorization.needed(), (req, res, next) => {
// POST /email/confirm takes the password confirmation token available as a
// payload parameter and if it verifies, it updates the confirmed_at date on the
// local profile.
router.post('/email/confirm', (req, res, next) => {
router.post('/email/verify', (req, res, next) => {
const {
token
@@ -115,19 +115,18 @@ router.put('/password/reset', (req, res, next) => {
});
});
router.put('/settings', authorization.needed(), (req, res, next) => {
const {
bio
} = req.body;
router.put('/displayname', authorization.needed(), (req, res, next) => {
UsersService
.updateSettings(req.user.id, {bio})
.editName(req.user.id, req.body.displayName)
.then(() => {
res.status(204).end();
})
.catch((err) => {
next(err);
.catch(error => {
if (error.code === 11000) {
next(errors.ErrDisplayTaken);
} else {
next(error);
}
});
});
+14 -8
View File
@@ -2,6 +2,7 @@ const express = require('express');
const passport = require('../../../services/passport');
const authorization = require('../../../middleware/authorization');
const errors = require('../../../errors');
const UsersService = require('../../../services/users');
const router = express.Router();
@@ -69,15 +70,20 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
return res.render('auth-callback', {err: JSON.stringify(errors.ErrNotAuthorized), data: null});
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
// Authorize the user to edit their displayName.
UsersService.toggleNameEdit(user.id, true)
.then(() => {
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
// We logged in the user! Let's send back the user data.
res.render('auth-callback', {err: null, data: JSON.stringify(user)});
});
// We logged in the user! Let's send back the user data.
res.render('auth-callback', {err: null, data: JSON.stringify(user)});
});
});
};
/**
+1
View File
@@ -13,6 +13,7 @@ router.use('/actions', authorization.needed(), require('./actions'));
router.use('/auth', require('./auth'));
router.use('/users', require('./users'));
router.use('/account', require('./account'));
router.use('/setup', require('./setup'));
// Bind the kue handler to the /kue path.
router.use('/kue', authorization.needed('ADMIN'), require('../../services/kue').kue.app);
+1 -1
View File
@@ -79,7 +79,7 @@ router.get('/comments/flagged', authorization.needed('ADMIN'), (req, res, next)
});
// Returns back all the users that are in the moderation queue.
router.get('/users/pending', (req, res, next) => {
router.get('/users/flagged', (req, res, next) => {
UsersService.moderationQueue()
.then((users) => {
return Promise.all([
+51
View File
@@ -0,0 +1,51 @@
const express = require('express');
const SetupService = require('../../../services/setup');
const router = express.Router();
router.get('/', (req, res, next) => {
SetupService
.isAvailable()
.then(() => {
res.json({installed: false});
})
.catch(() => {
res.json({installed: true});
});
});
router.post('/', (req, res, next) => {
SetupService
.isAvailable()
.then(() => {
// Allow the request to keep going here.
next();
})
.catch((err) => {
next(err);
});
}, (req, res, next) => {
const {
settings,
user: {email, password, displayName}
} = req.body;
SetupService
.setup({settings, user: {email, password, displayName}})
.then(() => {
// We're setup!
res.status(204).end();
})
.catch((err) => {
return next(err);
});
});
module.exports = router;
+19 -28
View File
@@ -1,7 +1,6 @@
const express = require('express');
const router = express.Router();
const UsersService = require('../../../services/users');
const SettingsService = require('../../../services/settings');
const CommentsService = require('../../../services/comments');
const mailer = require('../../../services/mailer');
const errors = require('../../../errors');
@@ -60,7 +59,16 @@ router.post('/:user_id/status', authorization.needed('ADMIN'), (req, res, next)
.catch(next);
});
router.post('/:user_id/email', authorization.needed('admin'), (req, res, next) => {
router.post('/:user_id/username-enable', authorization.needed('ADMIN'), (req, res, next) => {
UsersService
.toggleNameEdit(req.params.user_id, true)
.then(() => {
res.status(204).end();
});
});
router.post('/:user_id/email', authorization.needed('ADMIN'), (req, res, next) => {
UsersService.findById(req.params.user_id)
.then(user => {
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
@@ -88,12 +96,6 @@ router.post('/:user_id/email', authorization.needed('admin'), (req, res, next) =
.catch(next);
});
// /**
// * SendEmailConfirmation sends a confirmation email to the user.
// * @param {Request} req express request object
// * @param {String} email user email address
// */
/**
* SendEmailConfirmation sends a confirmation email to the user.
* @param {ExpressApp} app the instance of the express app
@@ -119,31 +121,20 @@ const SendEmailConfirmation = (app, userID, email, referer) => UsersService
// create a local user.
router.post('/', (req, res, next) => {
const {email, password, displayName} = req.body;
const redirectUri = req.header('Referer');
const redirectUri = req.header('X-Pym-Url') || req.header('Referer');
UsersService
.createLocalUser(email, password, displayName)
.then((user) => {
// Get the settings from the database to find out if we need to send an
// email confirmation. The Front end will know about the
// Send an email confirmation. The Front end will know about the
// requireEmailConfirmation as it's included in the settings get endpoint.
return SettingsService.retrieve().then(({requireEmailConfirmation = false}) => {
return SendEmailConfirmation(req.app, user.id, email, redirectUri)
.then(() => {
if (requireEmailConfirmation) {
SendEmailConfirmation(req.app, user.id, email, redirectUri)
.then(() => {
// Then send back the user.
res.status(201).json(user);
});
} else {
// We don't need to confirm the email, let's just send back the user!
// Then send back the user.
res.status(201).json(user);
}
});
});
})
.catch(err => {
next(err);
@@ -162,7 +153,7 @@ router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
// Set the user status to "pending" for review by moderators
if (action_type.slice(0, 4) === 'FLAG') {
return UsersService.setStatus(req.user.id, 'PENDING')
return UsersService.setStatus(req.params.user_id, 'PENDING')
.then(() => action);
} else {
return action;
@@ -178,9 +169,9 @@ router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
});
// trigger an email confirmation re-send by a new user
router.post('/resend-confirm', (req, res, next) => {
router.post('/resend-verify', (req, res, next) => {
const {email} = req.body;
const redirectUri = req.header('Referer');
const redirectUri = req.header('X-Pym-Url') || req.header('Referer');
if (!email) {
return next(errors.ErrMissingEmail);
+2
View File
@@ -1,9 +1,11 @@
const express = require('express');
const path = require('path');
const router = express.Router();
router.use('/api/v1', require('./api'));
router.use('/admin', require('./admin'));
router.use('/embed', require('./embed'));
router.get('/embed.js', (req, res) => res.sendFile(path.join(__dirname, '../client/coral-embed/index.js')));
router.use('/assets', require('./assets'));
router.get('/', (req, res) => {
+4 -4
View File
@@ -8,11 +8,11 @@ docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
deploy_tag() {
# Find our individual versions from the tags
if [ -n "$(echo $CIRCLE_TAG | grep -E '.*\..*\..*')" ]
if [ -n "$(echo $CIRCLE_TAG | grep -E 'v.*\..*\..*')" ]
then
major=$(echo $CIRCLE_TAG | cut -d. -f1)
minor=$(echo $CIRCLE_TAG | cut -d. -f2)
patch=$(echo $CIRCLE_TAG | cut -d. -f3)
major=$(echo ${CIRCLE_TAG//v} | cut -d. -f1)
minor=$(echo ${CIRCLE_TAG//v} | cut -d. -f2)
patch=$(echo ${CIRCLE_TAG//v} | cut -d. -f3)
major_version_tag=$major
minor_version_tag=$major.$minor
+100
View File
@@ -0,0 +1,100 @@
const UsersService = require('./users');
const SettingsService = require('./settings');
const SettingsModel = require('../models/setting');
const errors = require('../errors');
/**
* This service is used when we want to setup the application. It is consumed by
* the dynamic setup endpoint and by the cli-setup tool.
*/
module.exports = class SetupService {
/**
* This returns a promise which resolves if the setup is available.
*/
static isAvailable() {
// Check if we have an install lock present.
if (process.env.TALK_INSTALL_LOCK === 'TRUE') {
return Promise.reject(errors.ErrInstallLock);
}
// Get the current settings, we are expecing an error here.
return SettingsService
.retrieve()
.then(() => {
// We should NOT have gotten a settings object, this means that the
// application is already setup. Error out here.
return Promise.reject(errors.ErrSettingsInit);
})
.catch((err) => {
// If the error is `not init`, then we're good, otherwise, it's something
// else.
if (err !== errors.ErrSettingsNotInit) {
return Promise.reject(err);
}
// Allow the request to keep going here.
return;
});
}
/**
* This verifies that the current input for the setup is valid.
*/
static validate({settings, user: {email, displayName, password}}) {
// Verify the email address of the user.
if (!email) {
return Promise.reject(errors.ErrMissingEmail);
}
// Create a settings model to use for validation.
let settingsModel = new SettingsModel(settings);
// Verify other properties of the user.
return Promise.all([
UsersService.isValidDisplayName(displayName, false),
UsersService.isValidPassword(password),
settingsModel.validate()
]);
}
/**
* This will perform the setup.
*/
static setup({settings, user: {email, password, displayName}}) {
// Validate the settings first.
return SetupService
.validate({settings, user: {email, password, displayName}})
.then(() => {
return SettingsService.update(settings);
})
.then((settings) => {
// Settings are created! Create the user.
// Create the user.
return UsersService
.createLocalUser(email, password, displayName)
// Grant them administrative privileges and confirm the email account.
.then((user) => {
return Promise.all([
UsersService.addRoleToUser(user.id, 'ADMIN'),
UsersService.confirmEmail(user.id, email)
])
.then(() => ({
settings,
user
}));
});
});
}
};
+81 -19
View File
@@ -170,10 +170,11 @@ module.exports = class UsersService {
/**
* Check the requested displayname for naughty words (currently in English) and special chars
* @param {String} displayName word to be checked for profanity
* @param {String} displayName word to be checked for profanity
* @param {Boolean} checkAgainstWordlist enables cheching against the wordlist
* @return {Promise} rejected if the machine's sensibilites are offended
*/
static isValidDisplayName(displayName) {
static isValidDisplayName(displayName, checkAgainstWordlist = true) {
const onlyLettersNumbersUnderscore = /^[A-Za-z0-9_]+$/;
if (!displayName) {
@@ -185,10 +186,19 @@ module.exports = class UsersService {
return Promise.reject(errors.ErrSpecialChars);
}
// check for profanity
return Wordlist.displayNameCheck(displayName);
if (checkAgainstWordlist) {
// check for profanity
return Wordlist.displayNameCheck(displayName);
}
// No errors found!
return Promise.resolve(displayName);
}
/**
* Performs validations for the password.
*/
static isValidPassword(password) {
if (!password) {
return Promise.reject(errors.ErrMissingPassword);
@@ -350,7 +360,16 @@ module.exports = class UsersService {
return Promise.reject(new Error(`status ${status} is not supported`));
}
return UserModel.update({id}, {$set: {status}});
return UserModel.update({
id,
status: {
$ne: 'APPROVED'
}
}, {
$set: {
status
}
});
}
/**
@@ -447,6 +466,8 @@ module.exports = class UsersService {
.verifyToken(token, {
subject: PASSWORD_RESET_JWT_SUBJECT
})
// TODO: add search by __v as well
.then((decoded) => UsersService.findById(decoded.userId));
}
@@ -598,25 +619,33 @@ module.exports = class UsersService {
subject: EMAIL_CONFIRM_JWT_SUBJECT
})
.then(({userID, email, referer}) => {
return UserModel
.update({
id: userID,
profiles: {
$elemMatch: {
id: email,
provider: 'local'
}
}
}, {
$set: {
'profiles.$.metadata.confirmed_at': new Date()
}
})
return UsersService
.confirmEmail(userID, email)
.then(() => ({userID, email, referer}));
});
}
/**
* Marks the email on the user as confirmed.
*/
static confirmEmail(id, email) {
return UserModel
.update({
id: id,
profiles: {
$elemMatch: {
id: email,
provider: 'local'
}
}
}, {
$set: {
'profiles.$.metadata.confirmed_at': new Date()
}
});
}
/**
* Returns all users with pending 'ADMIN'ation actions.
* @return {Promise}
@@ -625,4 +654,37 @@ module.exports = class UsersService {
return UserModel.find({status: 'PENDING'});
}
/**
* Gives the user the ability to edit their username.
* @param {String} id the id of the user to be toggled.
* @param {Boolean} canEditName sets whether the user can edit their name.
* @return {Promise}
*/
static toggleNameEdit(id, canEditName) {
return UserModel.update({id}, {
$set: {canEditName}
});
}
/**
* Updates the user's displayName.
* @param {String} id the id of the user to be enabled.
* @param {String} displayName The new displayname for the user.
* @return {Promise}
*/
static editName(id, displayName) {
return UserModel.update({
id,
canEditName: true
}, {
$set: {
displayName: displayName.toLowerCase(),
canEditName: false,
status: 'PENDING'
}
}).then((result) => {
return result.nModified > 0 ? result :
Promise.reject(new Error('You do not have permission to update your username.'));
});
}
};
+1 -2
View File
@@ -10,8 +10,7 @@ const embedStreamCommands = {
return this
.waitForElementVisible('@moderationList')
.waitForElementVisible('@approveButton')
.click('@approveButton')
.waitForElementNotPresent('@approveButton');
.click('@approveButton');
}
};
+7 -2
View File
@@ -6,8 +6,8 @@ const embedStreamCommands = {
ready() {
return this
.waitForElementVisible('body', 4000)
.waitForElementVisible('iframe#coralStreamIframe')
.api.frame('coralStreamIframe');
.waitForElementVisible('#coralStreamEmbed > iframe')
.api.frame('coralStreamEmbed_iframe');
},
signUp(user) {
return this
@@ -22,6 +22,8 @@ const embedStreamCommands = {
.setValue('@signUpDialogDisplayName', user.displayName)
.waitForElementVisible('@signUpButton')
.click('@signUpButton')
.waitForElementVisible('@signInViewTrigger')
.click('@signInViewTrigger')
.waitForElementVisible('@logInButton')
.click('@logInButton')
.waitForElementVisible('@logoutButton', 5000);
@@ -102,6 +104,9 @@ module.exports = {
signUpButton: {
selector: '#coralSignUpButton'
},
signInViewTrigger: {
selector: '#coralSignInViewTrigger'
},
logoutButton: {
selector: '.commentStream #logout'
},
+10 -7
View File
@@ -4,8 +4,10 @@ const mocks = require('../mocks');
const mockComment = 'I read the comments';
const mockReply = 'This is a test reply';
const mockUser = {
email: `${new Date().getTime()}@test.com`,
name: 'testuser',
email: `${Date.now()}@test.com`,
name: `testuser${Math.random()
.toString()
.slice(-5)}`,
pw: 'testtest'
};
@@ -26,7 +28,7 @@ module.exports = {
// Load Page
client.resizeWindow(1200, 800)
.url(client.globals.baseUrl)
.frame('coralStreamIframe')
.frame('coralStreamEmbed_iframe')
// Register and Log In
.waitForElementVisible('#coralSignInButton', 2000)
@@ -39,6 +41,7 @@ module.exports = {
.setValue('#password', mockUser.pw)
.setValue('#confirmPassword', mockUser.pw)
.click('#coralSignUpButton')
.pause(5000)
.waitForElementVisible('#coralLogInButton', 10000)
.click('#coralLogInButton')
.waitForElementVisible('.coral-plugin-commentbox-button', 4000)
@@ -65,7 +68,7 @@ module.exports = {
// Load Page
client.url(client.globals.baseUrl)
.frame('coralStreamIframe');
.frame('coralStreamEmbed_iframe');
// Post a comment
client.waitForElementVisible('.coral-plugin-commentbox-button', 2000)
@@ -91,7 +94,7 @@ module.exports = {
// Load Page
client.resizeWindow(1200, 800)
.url(client.globals.baseUrl)
.frame('coralStreamIframe');
.frame('coralStreamEmbed_iframe');
// Post a comment
client.waitForElementVisible('.coral-plugin-commentbox-button', 2000)
@@ -138,7 +141,7 @@ module.exports = {
// Load Page
client.resizeWindow(1200, 800)
.url(client.globals.baseUrl)
.frame('coralStreamIframe');
.frame('coralStreamEmbed_iframe');
// Post a reply
client.waitForElementVisible('.coral-plugin-replies-reply-button', 5000)
@@ -161,7 +164,7 @@ module.exports = {
'Total comment count premod on': client => {
client.perform((client, done) => {
client.url(client.globals.baseUrl)
.frame('coralStreamIframe');
.frame('coralStreamEmbed_iframe');
// Verify that comment count is correct
client.waitForElementVisible('.coral-plugin-comment-count-text', 2000)
+71
View File
@@ -0,0 +1,71 @@
const passport = require('../../../passport');
const app = require('../../../../app');
const chai = require('chai');
const expect = chai.expect;
const SettingsService = require('../../../../services/settings');
const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
// Setup chai.
chai.should();
chai.use(require('chai-http'));
const UsersService = require('../../../../services/users');
describe('/api/v1/account/displayname', () => {
let mockUser;
beforeEach(() => SettingsService.init(settings).then(() => {
return UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana');
})
.then((user) => {
mockUser = user;
}));
describe('#put', () => {
it('it should enable a user to edit their username if canEditName is enabled', () => {
return chai.request(app)
.post(`/api/v1/users/${mockUser.id}/username-enable`)
.set(passport.inject({id: '456', roles: ['ADMIN']}))
.then(() => chai.request(app)
.put('/api/v1/account/displayname')
.set(passport.inject({id: mockUser.id, roles: []}))
.send({displayName: 'MojoJojo'}))
.then((res) => {
expect(res).to.have.status(204);
});
});
it('it should return an error if the wrong user tries to edit a username', (done) => {
chai.request(app)
.post(`/api/v1/users/${mockUser.id}/username-enable`)
.set(passport.inject({id: '456', roles: ['ADMIN']}))
.then(() => chai.request(app)
.put('/api/v1/account/displayname')
.set(passport.inject({id: 'wrongid', roles: []}))
.send({displayName: 'MojoJojo'}))
.then(() => {
done(new Error('Exected Error'));
})
.catch((err) => {
expect(err).to.be.truthy;
done();
});
});
it('it should return an error when the user tries to edit their username if canEditName is disabled', (done) => {
chai.request(app)
.put('/api/v1/account/displayname')
.set(passport.inject({id: mockUser.id, roles: []}))
.send({username: 'MojoJojo'})
.then(() => {
done(new Error('Exected Error'));
})
.catch((err) => {
expect(err).to.be.truthy;
done();
});
});
});
});
+1 -1
View File
@@ -110,7 +110,7 @@ describe('/api/v1/queue', () => {
it('should return all pending users and actions', function(done){
chai.request(app)
.get('/api/v1/queue/users/pending')
.get('/api/v1/queue/users/flagged')
.set(passport.inject({roles: ['ADMIN']}))
.end(function(err, res){
expect(err).to.be.null;
+31 -16
View File
@@ -52,33 +52,48 @@ describe('/api/v1/users/:user_id/email/confirm', () => {
describe('/api/v1/users/:user_id/actions', () => {
const users = [{
displayName: 'Ana',
email: 'ana@gmail.com',
password: '123456789'
}, {
displayName: 'Maria',
email: 'maria@gmail.com',
password: '123456789'
}];
let mockUser;
beforeEach(() => {
return SettingsService.init(settings).then(() => {
return UsersService.createLocalUsers(users);
});
});
beforeEach(() => SettingsService.init(settings).then(() => {
return UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana');
})
.then((user) => {
mockUser = user;
}));
describe('#post', () => {
it('it should update actions', () => {
return chai.request(app)
.post('/api/v1/users/abc/actions')
.post(`/api/v1/users/${mockUser.id}/actions`)
.set(passport.inject({id: '456', roles: ['ADMIN']}))
.send({'action_type': 'FLAG', metadata: {reason: 'Bio is too awesome.'}})
.then((res) => {
expect(res).to.have.status(201);
expect(res).to.have.body;
expect(res.body).to.have.property('action_type', 'FLAG');
expect(res.body).to.have.property('item_id', 'abc');
expect(res.body).to.have.property('item_id', mockUser.id);
});
});
});
});
describe('/api/v1/users/:user_id/username-enable', () => {
let mockUser;
beforeEach(() => SettingsService.init(settings).then(() => {
return UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana');
})
.then((user) => {
mockUser = user;
}));
describe('#post', () => {
it('it should enable a user to edit their username', () => {
return chai.request(app)
.post(`/api/v1/users/${mockUser.id}/username-enable`)
.set(passport.inject({id: '456', roles: ['ADMIN']}))
.then((res) => {
expect(res).to.have.status(204);
});
});
});
+52
View File
@@ -208,4 +208,56 @@ describe('services.UsersService', () => {
});
});
});
describe('#toggleNameEdit', () => {
it('should toggle the canEditName field', () => {
return UsersService
.toggleNameEdit(mockUsers[0].id, true)
.then(() => UsersService.findById(mockUsers[0].id))
.then((user) => {
expect(user).to.have.property('canEditName', true);
});
});
});
describe('#editName', () => {
it('should let the user edit their username if the proper toggle is set', () => {
return UsersService
.toggleNameEdit(mockUsers[0].id, true)
.then(() => UsersService.editName(mockUsers[0].id, 'Jojo'))
.then(() => UsersService.findById(mockUsers[0].id))
.then((user) => {
expect(user).to.have.property('displayName', 'jojo');
expect(user).to.have.property('canEditName', false);
});
});
it('should return an error if canEditName is false', (done) => {
UsersService
.editName(mockUsers[0].id, 'Jojo')
.then(() => UsersService.findById(mockUsers[0].id))
.then(() => {
done(new Error('Error expected'));
})
.catch((err) => {
expect(err).to.be.truthy;
done();
});
});
it('should return an error if the username is already taken', (done) => {
UsersService
.toggleNameEdit(mockUsers[0].id, true)
.then(() => UsersService.editName(mockUsers[0].id, 'Marvel'))
.then(() => UsersService.findById(mockUsers[0].id))
.then(() => {
done(new Error('Error expected'));
})
.catch((err) => {
expect(err).to.be.truthy;
done();
});
});
});
});

Some files were not shown because too many files have changed in this diff Show More