Added BE for setup

- Adjusted docs
- Fixed issues with process terminating wrong
This commit is contained in:
Wyatt Johnson
2017-02-03 11:05:46 -07:00
parent 80241025e5
commit f488b0e02d
17 changed files with 454 additions and 186 deletions
+7 -9
View File
@@ -1,9 +1,10 @@
# Contributor's Guide
Welcome! We are very excited that you are interested in contributing to Talk.
Welcome! We are very excited that you are interested in contributing to Talk.
This document is a companion to help you approach contributing. If it does not do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)!
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct.html).
## Product Roadmap
@@ -13,7 +14,7 @@ You can view product ideas and our longer term roadmap here https://trello.com/b
## Contribute to the documentation
Clear docs are a prerequisite for a successful open source project. We value non-code and code contributions equally.
Clear docs are a prerequisite for a successful open source project. We value non-code and code contributions equally.
We are looking for _documentarians_ to:
@@ -40,7 +41,7 @@ Talk is designed to integrate into existing environments in a variety of ways:
If you're considering deploying Talk, [please let us know](https://github.com/coralproject/talk/wiki/Contact-Us)! We are quite literally doing this for you and want to help you succeed any way we can.
If you are writing custom integration code in your fork of Talk, please consider keeping it generic and filing a Pull Request to contribute it back to the project! See our [forking and merging guidelines](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging) for more info.
If you are writing custom integration code in your fork of Talk, please consider keeping it generic and filing a Pull Request to contribute it back to the project! See our [forking and merging guidelines](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging) for more info.
## Write some code
@@ -48,9 +49,9 @@ First, [set up a dev environment](https://github.com/coralproject/talk/blob/mast
### Build a New Feature / Plugin
Talk is beginning life as a Commenting Platform, but is architected to support many varieties of community engagement.
Talk is beginning life as a Commenting Platform, but is architected to support many varieties of community engagement.
Please [contact us](https://github.com/coralproject/talk/wiki/Contact-Us) early and often if you'd like to help. We would love to hear your ideas for features and plugins and help you find a way to productively engage the project.
Please [contact us](https://github.com/coralproject/talk/wiki/Contact-Us) early and often if you'd like to help. We would love to hear your ideas for features and plugins and help you find a way to productively engage the project.
To get an idea of where the Coral Team is going, see:
@@ -66,13 +67,10 @@ Examples:
### Work on the Core
There is always more work to be done to make an application more stable, scaleable and secure.
There is always more work to be done to make an application more stable, scaleable and secure.
If you see issues in the code or have ideas on how we may improve Talk, please consider:
* [contributing a fix](https://github.com/coralproject/talk/wiki/Forking,-Branching-and-Merging),
* [filing an issue](https://github.com/coralproject/talk/issues), or
* or otherwise [letting us know](https://github.com/coralproject/talk/wiki/Contact-Us).
+60 -61
View File
@@ -1,95 +1,94 @@
# Installing a dev environment
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct.html).
# Installation
## Requirements
### System
- Any flavor of Linux, OSX or Windows
- Any flavour of Linux, OSX or Windows
- 1GB memory (minimum)
- 5GB storage (minimum)
### Software
## Installation From Source
* [Node](https://nodejs.org/es/download/package-manager) v7 or later
* Mongo v3.2 or later
* Redis v3.2 or later
### Requirements
There are some runtime requirements for running Talk from source:
- [Node](https://nodejs.org/) v7 or later
- [MongoDB](https://www.mongodb.com/) v3.2 or later
- [Redis](https://redis.io/) v3.2 or later
- [Yarn](https://yarnpkg.com/) v0.19.1 or later
_Please be sure to check the versions of these requirements. Insufficient versions of these may lead to unexpected errors!_
## First time setup
### Installing
### Installation
```bash
# Download the tarball containing the repository
curl -L https://github.com/coralproject/talk/tarball/master -o coralproject-talk.tar.gz
Navigate to a directory.
# Untar that file and change to that directory
tar xpf coralproject-talk.tar.gz
mv coralproject-talk-* coralproject-talk
cd coralproject-talk
```
git clone https://github.com/coralproject/talk
cd talk
yarn install
```
# Install package dependancies
yarn
### Environmental Variables
Talk uses environmental variables for configuration. You can learn about them in the [README file](README.md).
## Workflows
### The server
Starting the server:
```
yarn start
```
Browse to `http://localhost:3000` (or your custom port.)
### Building the front end
Our build process will build all front end components registered [here](https://github.com/coralproject/talk/blob/6052cac1d3494f8060325a88bb2ce03c88c2f94c/webpack.config.dev.js#L9-L15).
One time build:
```
# Build static files
yarn build
```
Build, then rebuild when a file is updated (development build):
### Running
```
yarn build-watch
Refer to the `README.md` file for required configuration variables to add to the
environment.
You can start the server after configuring the server using the command:
```bash
yarn start
```
You can see other scripts we've made available by consulting the `package.json`
file under the `scripts` key including:
### Testing
- `yarn test` run unit tests
- `yarn e2e` run end to end tests
- `yarn build-watch` watch for changes to client files and build static assets
- `yarn dev-start` watch for changes to server files and reload the server
Run all tests once:
## Installation From Docker Hub
`
yarn test
`
### Requirements
Run our end to end tests (will install Selenium and nightwatch):
There are some runtime requirements for running Talk for Docker:
`
yarn e2e
`
- [MongoDB](https://www.mongodb.com/) v3.2 or later
- [Redis](https://redis.io/) v3.2 or later
- [Docker](https://www.docker.com/) v1.13.0 or later
- [Docker Compose](https://docs.docker.com/compose/) v1.10.0 or later
_Please ensure all tests are passing before submitting a PR!_
_Please be sure to check the versions of these requirements. Insufficient versions of these may lead to unexpected errors!_
## Troubleshooting
### Installing
```bash
# Create a directory for talk
mkdir coralproject-talk
cd coralproject-talk
##### Can't ping the redis server!
# Download the docker-compose.yml file from the repository
curl -LO https://raw.githubusercontent.com/coralproject/talk/master/docker-compose.yml
```
- Check that Redis Server is running.
- Check that TALK_REDIS_URL is set.
##### Authenticaiton doesn't work!
- Make sure Redis is the correct version.
At this stage, you should refer to the `README.md` file for required
configuration variables to add to the environment key for the `talk` service
listed in the `docker-compose.yml` file.
### Running
```bash
# Start the services using compose
docker-compose up -d
```
+2 -1
View File
@@ -12,7 +12,7 @@ See our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CO
To set up a development environment or build from source, see [INSTALL.md](https://github.com/coralproject/talk/blob/master/INSTALL.md).
To launch a Talk server of your own from your browser without any need to muck about in a terminal or think about engineering concepts, stay tuned. We will launch [our installer](https://github.com/coralproject/talk-install) shortly!!
To launch a Talk server of your own from your browser without any need to muck about in a terminal or think about engineering concepts, stay tuned. We will launch [our installer](https://github.com/coralproject/talk-install) shortly!
### Configuration
@@ -37,6 +37,7 @@ Facebook Login enabled app.
- `TALK_SMTP_PASSWORD` (*required for email*) - password for the SMTP provider you are using.
- `TALK_SMTP_HOST` (*required for email*) - SMTP host url with format `smtp.domain.com`.
- `TALK_SMTP_PORT` (*required for email*) - SMTP port.
- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - Defaults to `FALSE`. When `TRUE`, disables the dynamic setup endpoint.
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
alternative methods of loading configuration during development.
+11 -15
View File
@@ -4,7 +4,7 @@
* Module dependencies.
*/
const util = require('../util');
// const util = require('./util');
const program = require('./commander');
program
@@ -15,18 +15,14 @@ program
.command('users', 'work with the application auth')
.parse(process.argv);
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
return;
}
// The ensures that the child process that is created here is always cleaned up
// properly when the parent process dies.
util.onshutdown([
(signal) => {
if ((program.runningCommand.killed === false) && (program.runningCommand.exitCode === null)) {
program.runningCommand.kill(signal);
}
/**
* When this provess exists, check to see if we have a running command, if we do
* check to see if it is still running. If it is, then kill it with a SIGINT
* signal. This is for the use case where we want to kill the process that is
* labled with the PID written out by the parent process.
*/
process.once('exit', () => {
if ((program.runningCommand.killed === false) && (program.runningCommand.exitCode === null)) {
program.runningCommand.kill('SIGINT');
}
]);
});
+1 -1
View File
@@ -10,7 +10,7 @@ const Table = require('cli-table');
const AssetModel = require('../models/asset');
const mongoose = require('../services/mongoose');
const scraper = require('../services/scraper');
const util = require('../util');
const util = require('./util');
// Register the shutdown criteria.
util.onshutdown([
+1 -1
View File
@@ -7,7 +7,7 @@
const program = require('./commander');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const util = require('../util');
const util = require('./util');
const mongoose = require('../services/mongoose');
const kue = require('../services/kue');
+2 -3
View File
@@ -2,13 +2,12 @@
const app = require('../app');
const program = require('./commander');
const debug = require('debug')('talk:server');
const http = require('http');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const kue = require('../services/kue');
const mongoose = require('../services/mongoose');
const util = require('../util');
const util = require('./util');
/**
* Get port from environment and store in Express.
@@ -79,7 +78,7 @@ function onListening() {
let bind = typeof addr === 'string'
? `pipe ${ addr}`
: `port ${ addr.port}`;
debug(`Listening on ${ bind}`);
console.log(`Listening on ${ bind}`);
}
/**
+148 -50
View File
@@ -9,7 +9,10 @@ const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const SettingModel = require('../models/setting');
const SettingsService = require('../services/settings');
const util = require('../util');
const SetupService = require('../services/setup');
const UsersService = require('../services/users');
const util = require('./util');
const errors = require('../errors');
// Register the shutdown criteria.
util.onshutdown([
@@ -29,60 +32,155 @@ 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');
if (program.defaults) {
return SettingsService.init();
}
return inquirer.prompt([
{
type: 'input',
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName,
validate: (input) => {
if (input && input.length > 0) {
return true;
}
// Get the current settings, we are expecing an error here.
return SettingsService
.retrieve()
.then(() => {
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'
// 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((answers) => {
// Update the settings that were changed.
Object.keys(answers).forEach((key) => {
if (answers[key] !== undefined) {
settings[key] = answers[key];
})
.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) => {
return settings.save();
// Update the settings that were changed.
Object.keys(answers).forEach((key) => {
if (answers[key] !== undefined) {
settings[key] = answers[key];
}
});
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
return inquirer.prompt([
{
type: 'input',
name: 'displayName',
message: 'Display Name',
filter: (displayName) => {
return UsersService
.isValidDisplayName(displayName, false)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'email',
message: 'Email',
format: 'email',
validate: (value) => {
if (value && value.length >= 3) {
return true;
}
return 'Email is required';
}
},
{
name: 'password',
message: 'Password',
type: 'password',
filter: (password) => {
return UsersService
.isValidPassword(password)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
filter: (confirmPassword) => {
return UsersService
.isValidPassword(confirmPassword)
.catch((err) => {
throw err.message;
});
}
},
]);
})
.then((user) => {
if (user.password !== user.confirmPassword) {
return Promise.reject(new Error('Passwords do not match'));
}
return SetupService.setup({
settings: settings.toObject(),
user: {
email: user.email,
displayName: user.displayName,
password: user.password
}
});
});
})
.then(({user}) => {
console.log('Settings created.');
console.log(`User ${user.id} created.`);
console.log('\nTalk is now installed!');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
})
.then(() => {
console.log('Talk is now installed!');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
};
// Start tthe setup process.
performSetup();
+1 -1
View File
@@ -9,7 +9,7 @@ const inquirer = require('inquirer');
const UsersService = require('../services/users');
const UserModel = require('../models/user');
const mongoose = require('../services/mongoose');
const util = require('../util');
const util = require('./util');
const Table = require('cli-table');
const validateRequired = (msg = 'Field is required', len = 1) => (input) => {
+12 -2
View File
@@ -2,7 +2,6 @@ const pkg = require('../package.json');
const dotenv = require('dotenv');
const fs = require('fs');
const program = require('commander');
const util = require('../util');
// Perform rewrites to the runtime environment variables based on the contents
// of the process.env.REWRITE_ENV if it exists. This is done here as it is the
@@ -27,6 +26,10 @@ const parseArgs = require('minimist')(process.argv.slice(2), {
}
});
/**
* If the config flag is present, then we have to load the configuration from
* the file specified. We will then load those values into the environment.
*/
if (parseArgs.config) {
let envConfig = dotenv.parse(fs.readFileSync(parseArgs.config, {encoding: 'utf8'}));
@@ -35,8 +38,15 @@ if (parseArgs.config) {
});
}
// If the `--pid` flag is used, put the current pid there.
/**
* If the pid flag is present, then we have to create a pid file at the location
* specified.
*/
if (parseArgs.pid) {
const util = require('./util');
console.log('Wrote PID');
util.pid(parseArgs.pid);
}
View File
+25
View File
@@ -0,0 +1,25 @@
version: '2'
services:
talk:
image: coralproject/talk:latest
restart: always
ports:
- "5000:5000"
depends_on:
- mongo
- redis
mongo:
image: mongo:3.2
restart: always
volumes:
- mongo:/data/db
redis:
image: redis:3.2
restart: always
volumes:
- redis:/data
volumes:
mongo:
external: false
redis:
external: false
+15 -1
View File
@@ -128,6 +128,18 @@ const ErrNotAuthorized = new APIError('not authorized', {
// initialized.
const ErrSettingsNotInit = new Error('settings not initialized, run `./bin/cli setup` to setup the application first');
// ErrSettingsInit is returned when the setup endpoint is hit and we are already
// initialized.
const ErrSettingsInit = new APIError('settings are already initialized', {
status: 500
});
// ErrInstallLock is returned when the setup endpoint is hit and the install
// lock is present.
const ErrInstallLock = new APIError('install lock active', {
status: 500
});
module.exports = {
ExtendableError,
APIError,
@@ -145,5 +157,7 @@ module.exports = {
ErrNotFound,
ErrInvalidAssetURL,
ErrAuthentication,
ErrNotAuthorized
ErrNotAuthorized,
ErrSettingsInit,
ErrInstallLock
};
+59
View File
@@ -0,0 +1,59 @@
const express = require('express');
const errors = require('../../../errors');
const SetupService = require('../../../services/setup');
const SettingsService = require('../../../services/settings');
const router = express.Router();
router.post('/', (req, res, next) => {
// Check if we have an install lock present.
if (process.env.TALK_INSTALL_LOCK === 'TRUE') {
return next(errors.ErrInstallLock);
}
// Get the current settings, we are expecing an error here.
SettingsService
.retrieve()
.then(() => {
// We should NOT have gotten a settings object, this means that the
// application is already setup. Error out here.
return next(errors.ErrSettingsInit);
})
.catch((err) => {
// If the error is `not init`, then we're good, otherwise, it's something
// else.
if (err !== errors.ErrSettingsNotInit) {
return next(err);
}
// Allow the request to keep going here.
return next();
});
}, (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;
+5 -23
View File
@@ -1,7 +1,6 @@
const express = require('express');
const router = express.Router();
const UsersService = require('../../../services/users');
const SettingsService = require('../../../services/settings');
const CommentsService = require('../../../services/comments');
const mailer = require('../../../services/mailer');
const errors = require('../../../errors');
@@ -88,12 +87,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
@@ -125,25 +118,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);
+67
View File
@@ -0,0 +1,67 @@
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 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
View File
@@ -170,10 +170,11 @@ module.exports = class UsersService {
/**
* Check the requested displayname for naughty words (currently in English) and special chars
* @param {String} displayName word to be checked for profanity
* @param {String} displayName word to be checked for profanity
* @param {Boolean} checkAgainstWordlist enables cheching against the wordlist
* @return {Promise} rejected if the machine's sensibilites are offended
*/
static isValidDisplayName(displayName) {
static isValidDisplayName(displayName, checkAgainstWordlist = true) {
const onlyLettersNumbersUnderscore = /^[A-Za-z0-9_]+$/;
if (!displayName) {
@@ -185,10 +186,19 @@ module.exports = class UsersService {
return Promise.reject(errors.ErrSpecialChars);
}
// check for profanity
return Wordlist.displayNameCheck(displayName);
if (checkAgainstWordlist) {
// check for profanity
return Wordlist.displayNameCheck(displayName);
}
// No errors found!
return Promise.resolve(displayName);
}
/**
* Performs validations for the password.
*/
static isValidPassword(password) {
if (!password) {
return Promise.reject(errors.ErrMissingPassword);
@@ -447,6 +457,8 @@ module.exports = class UsersService {
.verifyToken(token, {
subject: PASSWORD_RESET_JWT_SUBJECT
})
// TODO: add search by __v as well
.then((decoded) => UsersService.findById(decoded.userId));
}
@@ -598,25 +610,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}