mirror of
https://github.com/wassname/talk.git
synced 2026-07-24 13:20:47 +08:00
Initial pass at adding middleware for users
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user