mirror of
https://github.com/wassname/talk.git
synced 2026-07-16 11:22:16 +08:00
+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);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,19 @@ import Configure from 'containers/Configure/Configure';
|
||||
import Streams from 'containers/Streams/Streams';
|
||||
import CommunityContainer from 'containers/Community/CommunityContainer';
|
||||
import LayoutContainer from 'containers/LayoutContainer';
|
||||
import InstallContainer from 'containers/Install/InstallContainer';
|
||||
|
||||
const routes = (
|
||||
<Route path='/admin' component={LayoutContainer}>
|
||||
<IndexRoute component={ModerationContainer} />
|
||||
<Route path='embed' component={CommentStream} />
|
||||
<Route path='community' component={CommunityContainer} />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='streams' component={Streams} />
|
||||
</Route>
|
||||
<div>
|
||||
<Route exact path="/admin/install" component={InstallContainer}/>
|
||||
<Route path='/admin' component={LayoutContainer}>
|
||||
<IndexRoute component={ModerationContainer} />
|
||||
<Route path='embed' component={CommentStream} />
|
||||
<Route path='community' component={CommunityContainer} />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='streams' component={Streams} />
|
||||
</Route>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AppRouter = () => <Router history={browserHistory} routes={routes} />;
|
||||
|
||||
@@ -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}`));
|
||||
});
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import users from 'reducers/users';
|
||||
import auth from 'reducers/auth';
|
||||
import actions from 'reducers/actions';
|
||||
import assets from 'reducers/assets';
|
||||
import install from 'reducers/install';
|
||||
|
||||
// Combine all reducers into a main one
|
||||
export default combineReducers({
|
||||
@@ -15,5 +16,6 @@ export default combineReducers({
|
||||
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,5 +6,6 @@ export default {
|
||||
email: lang.t('error.email'),
|
||||
password: lang.t('error.password'),
|
||||
displayName: lang.t('error.displayName'),
|
||||
confirmPassword: lang.t('error.confirmPassword')
|
||||
confirmPassword: lang.t('error.confirmPassword'),
|
||||
organizationName: lang.t('error.organizationName'),
|
||||
};
|
||||
|
||||
@@ -2,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)
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"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",
|
||||
@@ -39,6 +40,7 @@
|
||||
"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",
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
@@ -97,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
|
||||
@@ -134,25 +127,14 @@ router.post('/', (req, res, next) => {
|
||||
.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);
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
+38
-18
@@ -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);
|
||||
@@ -456,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));
|
||||
}
|
||||
|
||||
@@ -607,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}
|
||||
|
||||
Reference in New Issue
Block a user