mirror of
https://github.com/wassname/talk.git
synced 2026-06-30 23:47:48 +08:00
Merge branch 'master' into recent-items
This commit is contained in:
+7
-9
@@ -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
@@ -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
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import React from 'react';
|
||||
import {Router, Route, IndexRoute, browserHistory} from 'react-router';
|
||||
|
||||
import ModerationContainer from 'containers/ModerationQueue/ModerationContainer';
|
||||
import CommentStream from 'containers/CommentStream/CommentStream';
|
||||
import Configure from 'containers/Configure/Configure';
|
||||
import Streams from 'containers/Streams/Streams';
|
||||
import CommunityContainer from 'containers/Community/CommunityContainer';
|
||||
import Configure from 'containers/Configure/Configure';
|
||||
import LayoutContainer from 'containers/LayoutContainer';
|
||||
import CommentStream from 'containers/CommentStream/CommentStream';
|
||||
import InstallContainer from 'containers/Install/InstallContainer';
|
||||
import CommunityContainer from 'containers/Community/CommunityContainer';
|
||||
import ModerationContainer from 'containers/ModerationQueue/ModerationContainer';
|
||||
|
||||
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} />;
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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}`));
|
||||
});
|
||||
};
|
||||
@@ -11,6 +11,7 @@ export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS';
|
||||
export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
|
||||
|
||||
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
|
||||
export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED';
|
||||
|
||||
export const fetchSettings = () => dispatch => {
|
||||
dispatch({type: SETTINGS_LOADING});
|
||||
@@ -33,6 +34,10 @@ export const updateWordlist = (listName, list) => {
|
||||
return {type: WORDLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const updateDomainlist = (listName, list) => {
|
||||
return {type: DOMAINLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const saveSettingsToServer = () => (dispatch, getState) => {
|
||||
let settings = getState().settings.toJS().settings;
|
||||
if (settings.charCount) {
|
||||
|
||||
@@ -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)});
|
||||
}
|
||||
|
||||
|
||||
+2
-30
@@ -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);
|
||||
@@ -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';
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
updateSettings,
|
||||
saveSettingsToServer,
|
||||
updateWordlist,
|
||||
updateDomainlist
|
||||
} from '../../actions/settings';
|
||||
|
||||
import {Button, List, Item} from 'coral-ui';
|
||||
@@ -14,6 +15,7 @@ import translations from '../../translations.json';
|
||||
import EmbedLink from './EmbedLink';
|
||||
import CommentSettings from './CommentSettings';
|
||||
import Wordlist from './Wordlist';
|
||||
import Domainlist from './Domainlist';
|
||||
import has from 'lodash/has';
|
||||
|
||||
class Configure extends Component {
|
||||
@@ -47,6 +49,11 @@ class Configure extends Component {
|
||||
this.props.dispatch(updateWordlist(listName, list));
|
||||
}
|
||||
|
||||
onChangeDomainlist = (listName, list) => {
|
||||
this.setState({changed: true});
|
||||
this.props.dispatch(updateDomainlist(listName, list));
|
||||
}
|
||||
|
||||
onSettingUpdate = (setting) => {
|
||||
this.setState({changed: true});
|
||||
this.props.dispatch(updateSettings(setting));
|
||||
@@ -73,7 +80,14 @@ class Configure extends Component {
|
||||
errors={this.state.errors}
|
||||
settingsError={this.onSettingError}/>;
|
||||
case 'embed':
|
||||
return <EmbedLink title={pageTitle} />;
|
||||
return has(this, 'props.settings.domains.whitelist')
|
||||
? <div>
|
||||
<Domainlist
|
||||
domains={this.props.settings.domains.whitelist}
|
||||
onChangeDomainlist={this.onChangeDomainlist}/>
|
||||
<EmbedLink title={pageTitle} />
|
||||
</div>
|
||||
: <EmbedLink title={pageTitle} />;
|
||||
case 'wordlist':
|
||||
return has(this, 'props.settings.wordlist')
|
||||
? <Wordlist
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
import styles from './Configure.css';
|
||||
import {Card} from 'coral-ui';
|
||||
|
||||
const Domainlist = ({domains, onChangeDomainlist}) => (
|
||||
<div>
|
||||
<h3>{lang.t('configure.domain-list-title')}</h3>
|
||||
<Card id={styles.domainlist}>
|
||||
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
|
||||
<TagsInput
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => onChangeDomainlist('whitelist', tags)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Domainlist;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
fetchFlaggedQueue,
|
||||
fetchModerationQueueComments,
|
||||
} from 'actions/comments';
|
||||
import {userStatusUpdate, sendNotificationEmail} from 'actions/users';
|
||||
import {userStatusUpdate, sendNotificationEmail, enableUsernameEdit} from 'actions/users';
|
||||
import {fetchSettings} from 'actions/settings';
|
||||
|
||||
import ModerationQueue from './ModerationQueue';
|
||||
@@ -122,7 +122,8 @@ const mapDispatchToProps = dispatch => {
|
||||
userStatusUpdate: (status, userId, commentId) => dispatch(userStatusUpdate(status, userId, commentId)).then(() => {
|
||||
dispatch(fetchModerationQueueComments());
|
||||
}),
|
||||
suspendUser: (userId, subject, text) => dispatch(userStatusUpdate('suspended', userId))
|
||||
suspendUser: (userId, subject, text) => dispatch(userStatusUpdate('BANNED', userId))
|
||||
.then(() => dispatch(enableUsernameEdit(userId)))
|
||||
.then(() => dispatch(sendNotificationEmail(userId, subject, text)))
|
||||
.then(() => dispatch(fetchModerationQueueComments()))
|
||||
,
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {render} from 'react-dom';
|
||||
import {ApolloProvider} from 'react-apollo';
|
||||
|
||||
import {client} from './services/client';
|
||||
import store from './services/store';
|
||||
|
||||
import App from './components/App';
|
||||
|
||||
// Render the application into the DOM
|
||||
ReactDOM.render(<App />, document.querySelector('#root'));
|
||||
render(
|
||||
<ApolloProvider client={client} store={store}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
, document.querySelector('#root')
|
||||
);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import {combineReducers} from 'redux';
|
||||
import auth from 'reducers/auth';
|
||||
import users from 'reducers/users';
|
||||
import assets from 'reducers/assets';
|
||||
import actions from 'reducers/actions';
|
||||
import install from 'reducers/install';
|
||||
import comments from 'reducers/comments';
|
||||
import settings from 'reducers/settings';
|
||||
import community from 'reducers/community';
|
||||
import users from 'reducers/users';
|
||||
import auth from 'reducers/auth';
|
||||
import actions from 'reducers/actions';
|
||||
import assets from 'reducers/assets';
|
||||
|
||||
// Combine all reducers into a main one
|
||||
export default combineReducers({
|
||||
export default {
|
||||
settings,
|
||||
comments,
|
||||
community,
|
||||
auth,
|
||||
actions,
|
||||
assets,
|
||||
users
|
||||
});
|
||||
users,
|
||||
install
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ const initialState = Map({
|
||||
wordlist: Map({
|
||||
banned: List(),
|
||||
suspect: List()
|
||||
}),
|
||||
domains: Map({
|
||||
whitelist: List()
|
||||
})
|
||||
}),
|
||||
saveSettingsError: null,
|
||||
@@ -24,6 +27,7 @@ export default (state = initialState, action) => {
|
||||
case types.SAVE_SETTINGS_SUCCESS: return saveComplete(state, action);
|
||||
case types.SAVE_SETTINGS_FAILED: return settingsSaveFailed(state, action);
|
||||
case types.WORDLIST_UPDATED: return updateWordlist(state, action);
|
||||
case types.DOMAINLIST_UPDATED: return updateDomainlist(state, action);
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
@@ -40,6 +44,10 @@ const updateWordlist = (state, action) => {
|
||||
return state.setIn(['settings', 'wordlist', action.listName], action.list);
|
||||
};
|
||||
|
||||
const updateDomainlist = (state, action) => {
|
||||
return state.setIn(['settings', 'domains', action.listName], action.list);
|
||||
};
|
||||
|
||||
const saveComplete = (state, action) => {
|
||||
const s = state.set('fetchingSettings', false).set('saveSettingsError', null);
|
||||
const settings = s.get('settings').merge(action.settings);
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
|
||||
import {createStore, applyMiddleware} from 'redux';
|
||||
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from 'reducers';
|
||||
import mainReducer from '../reducers';
|
||||
import {client} from './client';
|
||||
|
||||
/**
|
||||
* Create the store by merging the app reducers with
|
||||
* the talk adapter. The talk adapter is the wire between
|
||||
* this client and the coral backend. The idea is we can
|
||||
* write different adapters for other platforms if we want
|
||||
*/
|
||||
const middlewares = [
|
||||
applyMiddleware(client.middleware()),
|
||||
applyMiddleware(thunk)
|
||||
];
|
||||
|
||||
if (window.devToolsExtension) {
|
||||
|
||||
// we can't have the last argument of compose() be undefined
|
||||
middlewares.push(window.devToolsExtension());
|
||||
}
|
||||
|
||||
export default createStore(
|
||||
mainReducer,
|
||||
window.devToolsExtension && window.devToolsExtension(),
|
||||
applyMiddleware(thunk)
|
||||
combineReducers({
|
||||
...mainReducer,
|
||||
apollo: client.reducer()
|
||||
}),
|
||||
{},
|
||||
compose(...middlewares)
|
||||
);
|
||||
|
||||
@@ -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.",
|
||||
@@ -79,7 +79,9 @@
|
||||
"comment-count-header": "Limit Comment Length",
|
||||
"comment-count-text-pre": "Comments will be limited to ",
|
||||
"comment-count-text-post": " characters.",
|
||||
"comment-count-error": "Please enter a valid number."
|
||||
"comment-count-error": "Please enter a valid number.",
|
||||
"domain-list-title": "Domain Whitelist",
|
||||
"domain-list-text": "Some instructions on how to type the urls."
|
||||
},
|
||||
"bandialog": {
|
||||
"ban_user": "Ban User?",
|
||||
@@ -187,7 +189,12 @@
|
||||
"comment-count-header": "Limitar el largo del comentario",
|
||||
"comment-count-text-pre": "El largo de comentarios será ",
|
||||
"comment-count-text-post": " caracteres",
|
||||
"comment-count-error": "Por favor escribe un número válido."
|
||||
"comment-count-error": "Por favor escribe un número válido.",
|
||||
"domain-list-title": "Lista de Dominios Permitidos",
|
||||
"domain-list-text": "Instrucciones de como ingresar las URLs."
|
||||
},
|
||||
"embedlink": {
|
||||
"copy": "Copiar"
|
||||
},
|
||||
"bandialog": {
|
||||
"ban_user": "Quieres suspender el Usuario?",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
@@ -84,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
|
||||
} : {};
|
||||
@@ -100,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
|
||||
@@ -109,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
|
||||
@@ -122,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
|
||||
@@ -131,7 +138,7 @@ 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}
|
||||
@@ -148,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>
|
||||
);
|
||||
}
|
||||
@@ -193,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)
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import {ApolloProvider} from 'react-apollo';
|
||||
|
||||
import {client} from 'coral-framework/client';
|
||||
import store from 'coral-framework/store';
|
||||
import {client} from 'coral-framework/services/client';
|
||||
import store from 'coral-framework/services/store';
|
||||
|
||||
import Embed from './Embed';
|
||||
|
||||
|
||||
@@ -15,15 +15,15 @@ export const hideCreateDisplayNameDialog = () => ({type: actions.HIDE_CREATEDISP
|
||||
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 updateDisplayName = ({displayName}) => ({type: actions.UPDATE_DISPLAYNAME, displayName});
|
||||
|
||||
export const createDisplayName = (userId, formData) => dispatch => {
|
||||
dispatch(createDisplayNameRequest());
|
||||
coralApi(`/users/${userId}/displayname`, {method: 'POST', body: formData})
|
||||
.then((user) => {
|
||||
coralApi('/account/displayname', {method: 'PUT', body: formData})
|
||||
.then(() => {
|
||||
dispatch(createDisplayNameSuccess());
|
||||
dispatch(hideCreateDisplayNameDialog());
|
||||
dispatch(updateDisplayName(user));
|
||||
dispatch(updateDisplayName(formData));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(createDisplayNameFailure(lang.t(`error.${error.message}`)));
|
||||
@@ -43,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());
|
||||
@@ -58,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
|
||||
@@ -117,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}`)));
|
||||
@@ -186,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());
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -41,8 +41,7 @@ export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
|
||||
|
||||
export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN';
|
||||
|
||||
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';
|
||||
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';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
|
||||
@@ -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,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)
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import store from './store';
|
||||
import pym from './PymConnection';
|
||||
import store from './services/store';
|
||||
import pym from './services/PymConnection';
|
||||
import I18n from './modules/i18n/i18n';
|
||||
import * as authActions from './actions/auth';
|
||||
import * as assetActions from './actions/asset';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Map} from 'immutable';
|
||||
import {Map, fromJS} from 'immutable';
|
||||
import * as actions from '../constants/auth';
|
||||
|
||||
const initialState = Map({
|
||||
@@ -12,16 +12,16 @@ const initialState = Map({
|
||||
error: '',
|
||||
passwordRequestSuccess: null,
|
||||
passwordRequestFailure: null,
|
||||
emailConfirmationFailure: false,
|
||||
emailConfirmationLoading: false,
|
||||
emailConfirmationSuccess: 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) {
|
||||
@@ -38,9 +38,9 @@ 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 :
|
||||
@@ -131,18 +131,19 @@ export default function auth (state = initialState, action) {
|
||||
.set('passwordRequestFailure', 'There was an error sending your password reset email. Please try again soon!')
|
||||
.set('passwordRequestSuccess', null);
|
||||
case actions.UPDATE_DISPLAYNAME:
|
||||
console.log('Action', action);
|
||||
return state
|
||||
.set('user', purge(action.displayName));
|
||||
case actions.EMAIL_CONFIRM_ERROR:
|
||||
.setIn(['user', 'displayName'], action.displayName);
|
||||
case actions.VERIFY_EMAIL_FAILURE:
|
||||
return state
|
||||
.set('emailConfirmationFailure', true)
|
||||
.set('emailConfirmationLoading', false);
|
||||
case actions.CONFIRM_EMAIL_REQUEST:
|
||||
return state.set('emailConfirmationLoading', true);
|
||||
case actions.CONFIRM_EMAIL_SUCCESS:
|
||||
.set('emailVerificationFailure', true)
|
||||
.set('emailVerificationLoading', false);
|
||||
case actions.VERIFY_EMAIL_REQUEST:
|
||||
return state.set('emailVerificationLoading', true);
|
||||
case actions.VERIFY_EMAIL_SUCCESS:
|
||||
return state
|
||||
.set('emailConfirmationSuccess', true)
|
||||
.set('emailConfirmationLoading', false);
|
||||
.set('emailVerificationSuccess', true)
|
||||
.set('emailVerificationLoading', false);
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import Pym from '../../node_modules/pym.js';
|
||||
import Pym from '../../../node_modules/pym.js';
|
||||
|
||||
const pym = new Pym.Child({polling: 100});
|
||||
export default pym;
|
||||
@@ -0,0 +1,14 @@
|
||||
import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import getNetworkInterface from './transport';
|
||||
|
||||
export const client = new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
queryTransformer: addTypename,
|
||||
dataIdFromObject: (result) => {
|
||||
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
||||
return result.__typename + result.id; // eslint-disable-line no-underscore-dangle
|
||||
}
|
||||
return null;
|
||||
},
|
||||
networkInterface: getNetworkInterface()
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from './reducers';
|
||||
import mainReducer from '../reducers';
|
||||
import {client} from './client';
|
||||
|
||||
const middlewares = [
|
||||
@@ -0,0 +1,16 @@
|
||||
import {print} from 'graphql-tag/printer';
|
||||
|
||||
// quick way to add the subscribe and unsubscribe functions to the network interface
|
||||
const addGraphQLSubscriptions = (networkInterface, wsClient) => {
|
||||
return Object.assign(networkInterface, {
|
||||
subscribe: (request, handler) => wsClient.subscribe({
|
||||
query: print(request.query),
|
||||
variables: request.variables,
|
||||
}, handler),
|
||||
unsubscribe: (id) => {
|
||||
wsClient.unsubscribe(id);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default addGraphQLSubscriptions;
|
||||
@@ -0,0 +1,11 @@
|
||||
import {createNetworkInterface} from 'apollo-client';
|
||||
|
||||
export default function getNetworkInterface(apiUrl = '/api/v1/graph/ql', headers = {}) {
|
||||
return new createNetworkInterface({
|
||||
uri: apiUrl,
|
||||
opts: {
|
||||
credentials: 'same-origin',
|
||||
headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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/PymConnection';
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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.fetchSignUpFacebook} 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;
|
||||
|
||||
@@ -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 {
|
||||
@@ -42,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();
|
||||
}
|
||||
@@ -80,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(() => {
|
||||
|
||||
@@ -133,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'));
|
||||
@@ -146,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>
|
||||
@@ -157,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}
|
||||
@@ -175,12 +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()),
|
||||
|
||||
@@ -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',
|
||||
@@ -40,8 +42,10 @@ export default {
|
||||
},
|
||||
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',
|
||||
|
||||
@@ -30,6 +30,12 @@
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #E0E0E0;
|
||||
color: #4f5c67;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.type--black {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.Option {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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: () => {}
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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
@@ -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]
|
||||
@@ -153,7 +148,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
|
||||
|
||||
# Returns recent comments
|
||||
@@ -211,11 +206,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
|
||||
@@ -226,8 +216,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 {
|
||||
|
||||
@@ -62,6 +62,12 @@ const SettingSchema = new Schema({
|
||||
requireEmailConfirmation: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
domains: {
|
||||
whitelist: {
|
||||
type: Array,
|
||||
default: ['localhost']
|
||||
}
|
||||
}
|
||||
}, {
|
||||
timestamps: {
|
||||
|
||||
+8
-2
@@ -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'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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)});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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;
|
||||
+17
-34
@@ -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,15 +59,16 @@ router.post('/:user_id/status', authorization.needed('ADMIN'), (req, res, next)
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
router.post('/:user_id/displayname', authorization.needed(), (req, res, next) => {
|
||||
UsersService.setDisplayName(req.params.user_id, req.body.displayName)
|
||||
.then((user) => {
|
||||
res.status(201).json(user);
|
||||
})
|
||||
.catch(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) => {
|
||||
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');
|
||||
@@ -96,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
|
||||
@@ -127,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);
|
||||
@@ -170,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;
|
||||
@@ -186,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);
|
||||
|
||||
+17
-7
@@ -1,5 +1,7 @@
|
||||
const AssetModel = require('../models/asset');
|
||||
const SettingsService = require('./settings');
|
||||
const domainlist = require('./domainlist');
|
||||
const errors = require('../errors');
|
||||
|
||||
module.exports = class AssetsService {
|
||||
|
||||
@@ -53,16 +55,24 @@ module.exports = class AssetsService {
|
||||
* @return {Promise}
|
||||
*/
|
||||
static findOrCreateByUrl(url) {
|
||||
return AssetModel.findOneAndUpdate({url}, {url}, {
|
||||
|
||||
// Ensure that if it's new, we return the new object created.
|
||||
new: true,
|
||||
// Check the URL to confirm that is in the domain whitelist
|
||||
return domainlist.urlCheck(url).then((whitelisted) => {
|
||||
if (!whitelisted) {
|
||||
return Promise.reject(errors.ErrInvalidAssetURL);
|
||||
} else {
|
||||
return AssetModel.findOneAndUpdate({url}, {url}, {
|
||||
|
||||
// Perform an upsert in the event that this doesn't exist.
|
||||
upsert: true,
|
||||
// Ensure that if it's new, we return the new object created.
|
||||
new: true,
|
||||
|
||||
// Set the default values if not provided based on the mongoose models.
|
||||
setDefaultsOnInsert: true
|
||||
// Perform an upsert in the event that this doesn't exist.
|
||||
upsert: true,
|
||||
|
||||
// Set the default values if not provided based on the mongoose models.
|
||||
setDefaultsOnInsert: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
const debug = require('debug')('talk:services:domainlist');
|
||||
const _ = require('lodash');
|
||||
const SettingsService = require('./settings');
|
||||
|
||||
/**
|
||||
* The root domainlist object.
|
||||
* @type {Object}
|
||||
*/
|
||||
class Domainlist {
|
||||
|
||||
constructor() {
|
||||
this.lists = {
|
||||
whitelist: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads domains white list in from the database
|
||||
*/
|
||||
load() {
|
||||
return SettingsService
|
||||
.retrieve()
|
||||
.then((settings) => {
|
||||
|
||||
// Insert the settings domains whitelist.
|
||||
this.upsert(settings.domains);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the domains whitelist data
|
||||
* @param {Array} list list of domains to be set to the whitelist
|
||||
*/
|
||||
upsert(lists) {
|
||||
|
||||
// Add the domains to this array and also be sure are all unique domains
|
||||
if (!('whitelist' in lists)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lists['whitelist'] = Domainlist.parseList(lists['whitelist']);
|
||||
debug(`Added ${lists['whitelist'].length} domains to the whitelist.`);
|
||||
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the url to see if it contains any of the whitelisted domains.
|
||||
* @param {String} url value to match.
|
||||
* @return {Boolean} true if the url contains any of the domains, false otherwise.
|
||||
*/
|
||||
match(list, url) {
|
||||
|
||||
const domainToMatch = Domainlist.parseURL(url);
|
||||
|
||||
// This will return true in the event that at least one blockword is found
|
||||
// in the phrase.
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i] === domainToMatch) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// We've walked over all the whitelisted domains, and haven't had a
|
||||
// mismatch... It is not an allowed domain!
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the list content.
|
||||
* @param {Array} list array of domains to parse for a list.
|
||||
* @return {Array} the parsed list
|
||||
*/
|
||||
static parseList(list) {
|
||||
return _.uniq(list.map((domain) => Domainlist.parseURL(domain)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the URL.
|
||||
* @param {String} url url to parse for a domain.
|
||||
* @return {String} the domain
|
||||
*/
|
||||
static parseURL(url){
|
||||
let domain;
|
||||
|
||||
// removes protocol and get domain
|
||||
if (url.indexOf('://') > -1) {
|
||||
domain = url.split('/')[2];
|
||||
} else {
|
||||
domain = url.split('/')[0];
|
||||
}
|
||||
|
||||
// remove port number
|
||||
domain = domain.split(':')[0];
|
||||
|
||||
return domain.toLowerCase();
|
||||
}
|
||||
|
||||
static urlCheck(url) {
|
||||
const dl = new Domainlist();
|
||||
|
||||
return dl.load()
|
||||
.then(() => {
|
||||
return dl.match(dl.lists['whitelist'], url);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Domainlist;
|
||||
@@ -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
-38
@@ -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,26 +360,16 @@ module.exports = class UsersService {
|
||||
return Promise.reject(new Error(`status ${status} is not supported`));
|
||||
}
|
||||
|
||||
return UserModel.update({id}, {$set: {status}});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the display name of a user.
|
||||
* @param {String} id id of a user
|
||||
* @param {String} displayName display name to set
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
static setDisplayName(id, displayName) {
|
||||
|
||||
return UsersService.isValidDisplayName(displayName)
|
||||
.then(() => { // displayName is valid
|
||||
return UserModel.update(
|
||||
{id},
|
||||
{$set: {'displayName': displayName}})
|
||||
.then(() => {
|
||||
return UserModel.findOne({'id': id});
|
||||
});
|
||||
});
|
||||
return UserModel.update({
|
||||
id,
|
||||
status: {
|
||||
$ne: 'APPROVED'
|
||||
}
|
||||
}, {
|
||||
$set: {
|
||||
status
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -466,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));
|
||||
}
|
||||
|
||||
@@ -617,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}
|
||||
@@ -644,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.'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,8 +10,7 @@ const embedStreamCommands = {
|
||||
return this
|
||||
.waitForElementVisible('@moderationList')
|
||||
.waitForElementVisible('@approveButton')
|
||||
.click('@approveButton')
|
||||
.waitForElementNotPresent('@approveButton');
|
||||
.click('@approveButton');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user