Initial pass at adding middleware for users

This commit is contained in:
Wyatt Johnson
2016-11-10 17:14:33 -07:00
parent 5df5bf65b9
commit 6867760099
14 changed files with 352 additions and 49 deletions
+1
View File
@@ -5,6 +5,7 @@ RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Setup the environment
ENV NODE_ENV production
ENV PATH /usr/src/app/bin:$PATH
ENV TALK_PORT 5000
EXPOSE 5000
+10
View File
@@ -14,6 +14,16 @@ Run it once to install the dependencies.
`npm start`
Runs Talk.
### Configuration
The Talk application requires specific configuration options to be available
inside the environment in order to run, those variables are listed here:
- `TALK_SESSION_SECRET` (*required*) -
- `TALK_FACEBOOK_APP_ID` (*required*) -
- `TALK_FACEBOOK_APP_SECRET` (*required*) -
- `TALK_ROOT_URL` (*required*) - Root url of the installed application externally available in the format: `<scheme>://<host>` without the path.
### Running with Docker
Make sure you have Docker running first and then run `docker-compose up -d`
+52 -2
View File
@@ -2,6 +2,11 @@ const express = require('express');
const bodyParser = require('body-parser');
const morgan = require('morgan');
const path = require('path');
const helmet = require('helmet');
const passport = require('./passport');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('./redis');
const app = express();
@@ -12,12 +17,57 @@ if (app.get('env') !== 'test') {
app.use(morgan('dev'));
}
//==============================================================================
// APP MIDDLEWARE
//==============================================================================
app.set('trust proxy', 'loopback');
app.use(helmet());
app.use(bodyParser.json());
app.use('/client', express.static(path.join(__dirname, 'dist')));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// Routes.
app.use('/client', express.static(path.join(__dirname, 'dist')));
//==============================================================================
// SESSION MIDDLEWARE
//==============================================================================
const session_opts = {
secret: process.env.TALK_SESSION_SECRET,
httpOnly: true,
rolling: true,
saveUninitialized: false,
resave: false,
cookie: {
secure: false,
maxAge: 18000000, // 30 minutes for expiry.
},
store: new RedisStore({
ttl: 1800,
client: redis,
})
};
if (app.get('env') === 'production') {
// Enable the secure cookie when we are in production mode.
session_opts.cookie.secure = true;
}
app.use(session(session_opts));
//==============================================================================
// PASSPORT MIDDLEWARE
//==============================================================================
// Setup the PassportJS Middleware.
app.use(passport.initialize());
app.use(passport.session());
//==============================================================================
// ROUTES
//==============================================================================
app.use('/', require('./routes'));
//==============================================================================
+10
View File
@@ -10,8 +10,10 @@ services:
environment:
- "TALK_PORT=5000"
- "TALK_MONGO_URL=mongodb://mongo"
- "TALK_REDIS_URL=redis://redis"
depends_on:
- mongo
- redis
mongo:
image: mongo:3.2
@@ -19,6 +21,14 @@ services:
volumes:
- mongo:/data/db
redis:
image: redis:3.2-alpine
restart: always
volumes:
- redis:/data
volumes:
mongo:
external: false
redis:
external: false
+53
View File
@@ -0,0 +1,53 @@
/**
* authorization contains the references to the authorization middleware.
* @type {Object}
*/
const authorization = module.exports = {};
const debug = require('debug')('talk:middleware:authorization');
/**
* ErrNotAuthorized is an error that is returned in the event an operation is
* deemed not authorized.
* @type {Error}
*/
const ErrNotAuthorized = new Error('not authorized');
ErrNotAuthorized.status = 401;
// Add the ErrNotAuthorized error to the authorization object to be exported.
authorization.ErrNotAuthorized = ErrNotAuthorized;
/**
* has returns true if the user has all the roles specified, otherwise it will
* return false.
* @param {Object} user the user to check for roles
* @param {Array} roles all the roles that a user must have
* @return {Boolean} true if the user has all the roles required, false
* otherwise
*/
authorization.has = (user, ...roles) => roles.every((role) => user.roles.indexOf(role) >= 0);
/**
* needed is a connect middleware layer that ensures that all requests coming
* here are both authenticated and match a set of roles required to continue.
* @param {Array} roles all the roles that a user must have
* @return {Callback} connect middleware
*/
authorization.needed = (...roles) => (req, res, next) => {
// All routes that are wrapepd with this middleware actually require a role.
if (!req.user) {
debug(`No user on request, returning with ${ErrNotAuthorized}`);
return next(ErrNotAuthorized);
}
// Check to see if the current user has all the roles requested for the given
// array of roles requested, if one is not on the user, then this will
// evaluate to true.
if (!authorization.has(req.user, ...roles)) {
debug('User does not have all the required roles to access this page');
return next(ErrNotAuthorized);
}
// Looks like they're allowed!
return next();
};
+6 -10
View File
@@ -10,16 +10,12 @@ if (enabled('talk:db')) {
mongoose.set('debug', true);
}
try {
mongoose.connect(url, (err) => {
if (err) {
throw err;
}
mongoose.connect(url, (err) => {
if (err) {
throw err;
}
debug('Connected to MongoDB!');
});
} catch (err) {
console.error('Cannot stablish a connection with MongoDB', err);
}
debug('connection established');
});
module.exports = mongoose;
+10 -14
View File
@@ -9,7 +9,6 @@
"build-watch": "webpack --config webpack.config.dev.js --watch",
"lint": "eslint bin/* .",
"lint-fix": "eslint . --fix",
"pretest": "npm install",
"test": "mocha --compilers js:babel-core/register --recursive tests",
"test-watch": "mocha --compilers js:babel-core/register --recursive -w tests",
"embed-start": "npm run build && ./bin/www"
@@ -17,13 +16,8 @@
"config": {
"pre-git": {
"commit-msg": [],
"pre-commit": [
"npm run lint",
"npm test"
],
"pre-push": [
"npm test"
],
"pre-commit": ["npm run lint", "npm test"],
"pre-push": ["npm test"],
"post-commit": [],
"post-merge": []
}
@@ -32,12 +26,7 @@
"type": "git",
"url": "git+https://github.com/coralproject/talk.git"
},
"keywords": [
"talk",
"coral",
"coralproject",
"ask"
],
"keywords": ["talk", "coral", "coralproject", "ask"],
"author": "",
"license": "Apache-2.0",
"bugs": {
@@ -48,12 +37,19 @@
"bcrypt": "^0.8.7",
"body-parser": "^1.15.2",
"commander": "^2.9.0",
"connect-redis": "^3.1.0",
"debug": "^2.2.0",
"ejs": "^2.5.2",
"express": "^4.14.0",
"express-session": "^1.14.2",
"helmet": "^3.1.0",
"mongoose": "^4.6.5",
"morgan": "^1.7.0",
"passport": "^0.3.2",
"passport-facebook": "^2.1.1",
"passport-local": "^1.0.0",
"prompt": "^1.0.0",
"redis": "^2.6.3",
"uuid": "^2.0.3"
},
"devDependencies": {
+83
View File
@@ -0,0 +1,83 @@
const passport = require('passport');
const User = require('./models/user');
const LocalStrategy = require('passport-local').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy;
//==============================================================================
// SESSION SERIALIZATION
//==============================================================================
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User
.findById(id)
.then((user) => {
done(null, user);
})
.catch((err) => {
done(err);
});
});
/**
* Validates that a user is allowed to login.
* @param {User} user the user to be validated
* @param {Function} done the callback for the validation
*/
function ValidateUserLogin(user, done) {
if (!user) {
return done(new Error('user not found'));
}
if (user.disabled) {
return done(null, false, {message: 'Account disabled'});
}
return done(null, user);
}
//==============================================================================
// STRATEGIES
//==============================================================================
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
}, (email, password, done) => {
User
.findLocalUser(email, password)
.then((user) => {
if (!user) {
return done(null, false, {message: 'Incorrect email/password combination'});
}
return ValidateUserLogin(user, done);
})
.catch((err) => {
done(err);
});
}));
if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET && process.env.TALK_ROOT_URL) {
passport.use(new FacebookStrategy({
clientID: process.env.TALK_FACEBOOK_APP_ID,
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
callbackURL: `${process.env.TALK_ROOT_URL}/connect/facebook/callback`
}, (accessToken, refreshToken, profile, done) => {
User
.findOrCreateExternalUser(profile)
.then((user) =>
ValidateUserLogin(user, done)
)
.catch((err) => {
done(err);
});
}));
} else {
console.error('Facebook cannot be enabled, missing one of TALK_FACEBOOK_APP_ID, TALK_FACEBOOK_APP_SECRET, TALK_ROOT_URL');
}
module.exports = passport;
+43
View File
@@ -0,0 +1,43 @@
const redis = require('redis');
const debug = require('debug')('talk:redis');
const url = process.env.TALK_REDIS_URL || 'redis://localhost';
const client = redis.createClient(url, {
retry_strategy: function(options) {
if (options.error.code === 'ECONNREFUSED') {
// End reconnecting on a specific error and flush all commands with a individual error
return new Error('The server refused the connection');
}
if (options.total_retry_time > 1000 * 60 * 60) {
// End reconnecting after a specific timeout and flush all commands with a individual error
return new Error('Retry time exhausted');
}
if (options.times_connected > 10) {
// End reconnecting with built in error
return undefined;
}
// reconnect after
return Math.max(options.attempt * 100, 3000);
}
});
// client.on('error', (err) => {
// throw err;
// });
client.ping((err) => {
if (err) {
console.error('Can\'t ping the redis server!');
throw err;
}
debug('connection established');
});
module.exports = client;
+43
View File
@@ -0,0 +1,43 @@
const express = require('express');
const passport = require('../../../passport');
const authorization = require('../../../middleware/authorization');
const router = express.Router();
router.get('/', authorization.needed(), (req, res) => {
res.json(req.user);
});
router.delete('/', (req, res) => {
req.logout();
res.status(204).end();
});
/**
* Local auth endpoint, will recieve a email and password
*/
router.post('/local', (req, res, next) => {
// Perform the local authentication.
passport.authenticate('local', (err, user) => {
if (err) {
return next(err);
}
if (!user) {
return next(authorization.ErrNotAuthorized);
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return next(err);
}
// We logged in the user! Let's send back the user data.
res.json({user});
});
})(req, res, next);
});
module.exports = router;
+1
View File
@@ -3,6 +3,7 @@ const express = require('express');
const router = express.Router();
router.use('/asset', require('./asset'));
router.use('/auth', require('./auth'));
router.use('/comments', require('./comments'));
router.use('/settings', require('./settings'));
router.use('/stream', require('./stream'));
+1 -1
View File
@@ -6,7 +6,7 @@ router.use('/admin', require('./admin'));
router.use('/embed', require('./embed'));
router.get('/', (req, res) => {
return res.render('home', {});
res.render('home', {});
});
module.exports = router;
+39
View File
@@ -0,0 +1,39 @@
require('../../../utils/mongoose');
const app = require('../../../../app');
const chai = require('chai');
const expect = chai.expect;
chai.use(require('chai-http'));
const User = require('../../../../models/user');
describe('POST /auth/local', () => {
beforeEach(() => {
return User.createLocalUser('maria@gmail.com', 'password!', 'Maria');
});
it('should send back the user on a successful login', () => {
return chai.request(app)
.post('/api/v1/auth/local')
.send({email: 'maria@gmail.com', password: 'password!'})
.catch((res) => {
expect(res).to.have.status(200);
expect(res).to.be.json;
expect(res.body).to.have.property('user');
expect(res.body.user).to.have.property('displayName', 'Maria');
});
});
it('should not send back the user on a unsuccessful login', () => {
return chai.request(app)
.post('/api/v1/auth/local')
.send({email: 'maria@gmail.com', password: 'password!3'})
.catch((err) => {
expect(err).to.not.be.null;
expect(err.response).to.have.status(401);
expect(err.response.body).to.have.property('message', 'not authorized');
});
});
});
-22
View File
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<title>Talk - Coral Admin</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
<style media="screen">
body, #root {
width: 100%;
height: 100%;
margin: 0;
background: #fff;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="<%= basePath %>/bundle.js" charset="utf-8"></script>
</body>
</html>