Skip to content

Commit

Permalink
Merge branch 'feature/social-auth' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
gocreating committed Oct 23, 2016
2 parents 9af2719 + d9de2de commit ccaaa60
Show file tree
Hide file tree
Showing 15 changed files with 230 additions and 49 deletions.
4 changes: 4 additions & 0 deletions configs/project/server.js
Expand Up @@ -14,5 +14,9 @@ if (process.env.TRAVIS) {
},
mongo: require('./mongo/credential'),
firebase: require('./firebase/credential.json'),
passportStrategy: {
facebook: require('./passportStrategy/facebook/credential'),
linkedin: require('./passportStrategy/linkedin/credential'),
},
};
}
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -107,7 +107,9 @@
"multer": "^1.1.0",
"object-assign": "^4.1.0",
"passport": "^0.3.2",
"passport-facebook": "^2.1.1",
"passport-jwt": "^2.0.0",
"passport-linkedin-oauth2": "^1.4.1",
"pm2": "^2.0.18",
"react": "^15.3.2",
"react-bootstrap": "^0.30.5",
Expand Down
2 changes: 1 addition & 1 deletion specs/endToEnd/apis/user.js
Expand Up @@ -50,7 +50,7 @@ describe('#user', () => {
expect(res).to.not.be.undefined;
expect(res.status).to.equal(200);
expect(res.body.errors[0].code)
.to.equal(Errors.ODM_VALIDATION.code);
.to.equal(Errors.USER_EXISTED.code);
done();
});
});
Expand Down
31 changes: 30 additions & 1 deletion src/common/components/pages/user/LoginPage.js
@@ -1,12 +1,41 @@
import React from 'react';
import PageHeader from 'react-bootstrap/lib/PageHeader';
import Grid from 'react-bootstrap/lib/Grid';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
import PageLayout from '../../layouts/PageLayout';
import Head from '../../widgets/Head';
import LoginForm from '../../forms/LoginForm';

const LoginPage = (props) => (
<PageLayout>
<Head
links={[
'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-social/5.0.0/bootstrap-social.min.css',
]}
/>
<PageHeader>Login</PageHeader>
<LoginForm location={props.location} />
<Grid>
<Row>
<Col md={9}>
<LoginForm location={props.location} />
</Col>
<Col md={3}>
<a
href="/auth/facebook"
className="btn btn-block btn-social btn-facebook"
>
<span className="fa fa-facebook"></span>Login with Facebook
</a>
<a
href="/auth/linkedin"
className="btn btn-block btn-social btn-linkedin"
>
<span className="fa fa-linkedin"></span>Login with LinkedIn
</a>
</Col>
</Row>
</Grid>
</PageLayout>
);

Expand Down
1 change: 1 addition & 0 deletions src/common/constants/ErrorCodes.js
Expand Up @@ -3,6 +3,7 @@ export default {
ODM_OPERATION_FAIL: 'ODM_OPERATION_FAIL',
STATE_PRE_FETCHING_FAIL: 'STATE_PRE_FETCHING_FAIL',
USER_UNAUTHORIZED: 'USER_UNAUTHORIZED',
USER_EXISTED: 'USER_EXISTED',
PERMISSION_DENIED: 'PERMISSION_DENIED',
LOCALE_NOT_SUPPORTED: 'LOCALE_NOT_SUPPORTED',
USER_TOKEN_EXPIRATION: 'USER_TOKEN_EXPIRATION',
Expand Down
6 changes: 6 additions & 0 deletions src/common/constants/Errors.js
Expand Up @@ -31,6 +31,12 @@ export default {
title: 'User Unauthorized',
detail: 'Please login to access the resource.',
},
[ErrorCodes.USER_EXISTED]: {
code: ErrorCodes.USER_EXISTED,
status: 400,
title: 'User Existed',
detail: 'This user is already registered.',
},
[ErrorCodes.PERMISSION_DENIED]: {
code: ErrorCodes.PERMISSION_DENIED,
status: 403,
Expand Down
10 changes: 10 additions & 0 deletions src/server/controllers/socialAuth.js
@@ -0,0 +1,10 @@
import passport from 'passport';

export default {
initFacebook: passport.authenticate('facebook', {
scope: ['public_profile', 'email'],
}),
initLinkedin: passport.authenticate('linkedin', {
state: Math.random(),
}),
};
52 changes: 41 additions & 11 deletions src/server/controllers/user.js
@@ -1,6 +1,8 @@
import Errors from '../../common/constants/Errors';
import { handleDbError } from '../decorators/handleError';
import User from '../models/User';
import filterAttribute from '../utils/filterAttribute';
import { loginUser } from '../../common/actions/userActions';

export default {
list(req, res) {
Expand All @@ -20,17 +22,25 @@ export default {
},

create(req, res) {
const user = User({
name: req.body.name,
email: {
value: req.body.email,
},
password: req.body.password,
});
user.save(handleDbError(res)((user) => {
res.json({
user: user,
});
User.findOne({
'email.value': req.body.email,
}, handleDbError(res)((user) => {
if (user) {
res.errors([Errors.USER_EXISTED]);
} else {
const user = User({
name: req.body.name,
email: {
value: req.body.email,
},
password: req.body.password,
});
user.save(handleDbError(res)((user) => {
res.json({
user: user,
});
}));
}
}));
},

Expand Down Expand Up @@ -61,6 +71,26 @@ export default {
}));
},

socialLogin(req, res, next) {
let { user } = req;
let token = user.toJwtToken();

user.save(handleDbError(res)(() => {
req.store
.dispatch(loginUser({
token: token,
data: user,
}))
.then(() => {
let { token, user } = req.store.getState().cookies;

res.cookie('token', token);
res.cookie('user', user);
res.redirect('/');
});
}));
},

logout(req, res) {
req.logout();
res.json({});
Expand Down
4 changes: 2 additions & 2 deletions src/server/middlewares/index.js
Expand Up @@ -3,7 +3,7 @@ import path from 'path';
import express from 'express';
import favicon from 'serve-favicon';
import morgan from './morgan';
import passport from './passport';
import passportInit from './passportInit';
import mountStore from './mountStore';
import mountHelper from './mountHelper';
import initCookie from './initCookie';
Expand Down Expand Up @@ -44,5 +44,5 @@ export default ({ app }) => {
app.use(initCookie);

// setup passport
app.use(passport);
app.use(passportInit);
};
27 changes: 0 additions & 27 deletions src/server/middlewares/passport.js

This file was deleted.

15 changes: 15 additions & 0 deletions src/server/middlewares/passportAuth.js
@@ -0,0 +1,15 @@
import passport from 'passport';

export default (strategyName) => (req, res, next) => (
passport.authenticate(strategyName, {
failureRedirect: '/user/login',
session: false,
}, (err, user, info) => {
if (err || !user) {
return res.redirect('/user/login');
}
// mount user instance
req.user = user;
next();
})(req, res, next)
);
85 changes: 85 additions & 0 deletions src/server/middlewares/passportInit.js
@@ -0,0 +1,85 @@
import passport from 'passport';
import { Strategy as JwtStrategy } from 'passport-jwt';
import { Strategy as FacebookStrategy } from 'passport-facebook';
import { Strategy as OAuthLinkedinStrategy } from 'passport-linkedin-oauth2';
import configs from '../../../configs/project/server';
import User from '../models/User';

const cookieExtractor = (req) => {
return req.store.getState().cookies.token;
};

passport.use(new JwtStrategy({
jwtFromRequest: cookieExtractor,
secretOrKey: configs.jwt.secret,
}, (jwtPayload, done) => {
User.findById(jwtPayload._id, (err, user) => {
if (err) {
return done(err, false);
}
if (user) {
done(null, user);
} else {
done(null, false);
}
});
}));

function findOrCreateUser(schemaProfileKey, email, cb) {
if (!email) {
return cb(new Error('Email is required'));
}
User.findOne({ 'email.value': email }, (err, user) => {
if (err) {
return cb(err);
}
if (!user) {
user = new User();
}
if (!user.social.profile[schemaProfileKey]) {
user.social.profile[schemaProfileKey] = {};
}
return cb(null, user);
});
}

if (configs.passportStrategy.facebook) {
passport.use(new FacebookStrategy({
...configs.passportStrategy.facebook.default,
...configs.passportStrategy.facebook[process.env.NODE_ENV],
}, (req, accessToken, refreshToken, profile, done) => {
findOrCreateUser('facebook', profile._json.email, (err, user) => {
if (err) {
return done(err);
}
// map `facebook-specific` profile fields to our custom profile fields
user.social.profile.facebook = profile._json;
user.email.value = user.email.value || profile._json.email;
user.name = user.name || profile._json.name;
user.avatarURL = user.avatarURL || profile._json.picture.data.url;
done(null, user);
});
}));
}

if (configs.passportStrategy.linkedin) {
passport.use(new OAuthLinkedinStrategy({
...configs.passportStrategy.linkedin.default,
...configs.passportStrategy.linkedin[process.env.NODE_ENV],
}, (req, accessToken, refreshToken, profile, done) => {
findOrCreateUser('linkedin', profile._json.emailAddress, (err, user) => {
if (err) {
return done(err);
}
// map `linkedin-specific` profile fields to our custom profile fields
user.social.profile.linkedin = profile._json;
user.email.value = user.email.value || profile._json.emailAddress;
user.name = user.name || profile._json.formattedName;
user.avatarURL = user.avatarURL || profile._json.pictureUrl;
done(null, user);
});
}));
}

const passportInitMiddleware = passport.initialize();
export default passportInitMiddleware;
15 changes: 8 additions & 7 deletions src/server/models/User.js
Expand Up @@ -31,7 +31,8 @@ let UserSchema = new mongoose.Schema({
},
password: {
type: String,
required: true,
// there is no password for a social account
required: false,
set: hashPassword,
},
role: {
Expand All @@ -40,6 +41,12 @@ let UserSchema = new mongoose.Schema({
default: Roles.USER,
},
avatarURL: String,
social: {
profile: {
facebook: Object,
linkedin: Object,
},
},
}, {
versionKey: false,
timestamps: {
Expand All @@ -50,12 +57,6 @@ let UserSchema = new mongoose.Schema({

UserSchema.plugin(paginatePlugin);

UserSchema.path('email.value').validate(function(value, cb) {
User.findOne({ 'email.value': value }, (err, user) => {
cb(!err && !user);
});
}, 'This email address is already registered');

UserSchema.methods.auth = function(password, cb) {
const isAuthenticated = (this.password === hashPassword(password));
cb(null, isAuthenticated);
Expand Down
2 changes: 2 additions & 0 deletions src/server/routes/index.js
@@ -1,9 +1,11 @@
import apiRoutes from './api';
import socialAuthRoutes from './socialAuth';
import ssrFetchStateRoutes from './ssrFetchState';
import ssrRoutes from './ssr';

export default ({ app }) => {
apiRoutes({ app });
socialAuthRoutes({ app });
ssrFetchStateRoutes({ app });
ssrRoutes({ app });
};
23 changes: 23 additions & 0 deletions src/server/routes/socialAuth.js
@@ -0,0 +1,23 @@
import passportAuth from '../middlewares/passportAuth';
import socialAuthController from '../controllers/socialAuth';
import userController from '../controllers/user';
import configs from '../../../configs/project/server';

export default ({ app }) => {
// facebook
if (configs.passportStrategy.facebook) {
app.get('/auth/facebook', socialAuthController.initFacebook);
app.get('/auth/facebook/callback',
passportAuth('facebook'),
userController.socialLogin
);
}
// linkedin
if (configs.passportStrategy.linkedin) {
app.get('/auth/linkedin', socialAuthController.initLinkedin);
app.get('/auth/linkedin/callback',
passportAuth('linkedin'),
userController.socialLogin
);
}
};

0 comments on commit ccaaa60

Please sign in to comment.