Skip to content

Commit

Permalink
Initial pass at adding middleware for users
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattjoh committed Nov 11, 2016
1 parent 5df5bf6 commit 6867760
Show file tree
Hide file tree
Showing 14 changed files with 352 additions and 49 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
54 changes: 52 additions & 2 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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'));

//==============================================================================
Expand Down
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,25 @@ services:
environment:
- "TALK_PORT=5000"
- "TALK_MONGO_URL=mongodb://mongo"
- "TALK_REDIS_URL=redis://redis"
depends_on:
- mongo
- redis

mongo:
image: mongo:3.2
restart: always
volumes:
- mongo:/data/db

redis:
image: redis:3.2-alpine
restart: always
volumes:
- redis:/data

volumes:
mongo:
external: false
redis:
external: false
53 changes: 53 additions & 0 deletions middleware/authorization.js
Original file line number Diff line number Diff line change
@@ -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();
};
16 changes: 6 additions & 10 deletions mongoose.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
24 changes: 10 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,15 @@
"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"
},
"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": []
}
Expand All @@ -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": {
Expand All @@ -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": {
Expand Down
83 changes: 83 additions & 0 deletions passport.js
Original file line number Diff line number Diff line change
@@ -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 changes: 43 additions & 0 deletions redis.js
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 6867760

Please sign in to comment.