sails-hook-blacksails
Version:
A Sails Micro-app architecture framework
442 lines (398 loc) • 13.8 kB
JavaScript
import _ from 'lodash';
import crypto from 'crypto';
import passport from 'passport';
import url from 'url';
/**
* Passport Service
#
* A painless Passport.js service for your Sails app that is guaranteed to
* Rock Your Socks™. It takes all the hassle out of setting up Passport.js by
* encapsulating all the boring stuff in two functions:
#
* passport.endpoint()
* passport.callback()
#
* The former sets up an endpoint (/auth/:provider) for redirecting a user to a
* third-party provider for authentication, while the latter sets up a callback
* endpoint (/auth/:provider/callback) for receiving the response from the
* third-party provider. All you have to do is define in the configuration which
* third-party providers you'd like to support. It's that easy!
#
* Behind the scenes, the service stores all the data it needs within "Pass-
* ports". These contain all the information required to associate a local user
* with a profile from a third-party provider. This even holds true for the good
* ol' password authentication scheme – the Authentication Service takes care of
* encrypting passwords and storing them in Passports, allowing you to keep your
* User model free of bloat.
*/
passport.protocols = sails.config.protocols;
// passport.protocols = require('./protocols');
/**
* Connect a third-party profile to a local user
#
* This is where most of the magic happens when a user is authenticating with a
* third-party provider. What it does, is the following:
#
* 1. Given a provider and an identifier, find a matching Passport.
* 2. From here, the logic branches into two paths.
#
* - A user is not currently logged in:
* 1. If a Passport wasn't found, create a new user as well as a new
* Passport that will be assigned to the user.
* 2. If a Passport was found, get the user associated with the passport.
#
* - A user is currently logged in:
* 1. If a Passport wasn't found, create a new Passport and associate it
* with the already logged in user (ie. "Connect")
* 2. If a Passport was found, nothing needs to happen.
#
* As you can see, this function handles both "authentication" and "authori-
* zation" at the same time. This is due to the fact that we pass in
* `passReqToCallback: true` when loading the strategies, allowing us to look
* for an existing session in the request and taking action based on that.
#
* For more information on auth(entication|rization) in Passport.js, check out:
* http://passportjs.org/guide/authenticate/
* http://passportjs.org/guide/authorize/
#
* @param {Object} req
* @param {Object} query
* @param {Object} profile
* @param {Function} next
*/
passport.connect = async function connect(req, query, profile, next) {
try {
sails.log('=== passport.connect profile ===', profile);
sails.log('=== passport.connect query ===', query);
let provider;
let user = {};
provider = undefined;
query.provider = req.param('provider');
provider = profile.provider || query.provider;
if (!provider) {
return next(new Error('No authentication provider was identified.'));
}
if (_.has(profile, 'emails')) {
user.email = profile.emails[0].value || profile.emails[0];
} else if (_.has(profile, 'email')) {
user.email = profile.email;
}
const locale = profile._json.locale;
const token = query.tokens.accessToken;
const identifier = query.identifier;
const profileWithLocale = await FacebookService.getProfileWithLocale({ token, identifier, locale });
const profileWithLocaleHasName = _.has(profileWithLocale, 'first_name')
&& !_.isEmpty(profileWithLocale.first_name);
user.locale = locale;
if (profileWithLocaleHasName) {
// NOTE:
// VIPT 需要把 fitstName 以及 lastName 都合併到 firstName 中
// user.firstName = profileWithLocale.first_name;
// user.lastName = profileWithLocale.last_name;
user.firstName = `${profileWithLocale.last_name}${profileWithLocale.first_name}`;
} else {
// NOTE:
// VIPT 需要把 fitstName 以及 lastName 都合併到 firstName 中
// user.firstName = profile.name.givenName;
// user.lastName = profile.name.familyName;
user.firstName = `${profile.name.familyName}${profile.name.givenName}`;
}
if (_.has(profile, 'username') && !_.isEmpty(profile.username)) {
user.username = profile.username;
} else if (_.has(user, 'email') && !_.isEmpty(user.email)) {
user.username = user.email;
} else {
user.username = profile.id;
}
// 儲存 Facebook ID 和個人頭像照片
user.facebookId = profile.id;
user.avatar = `https://graph.facebook.com/${profile.id}/picture?redirect=true&height=470&width=470`;
if (_.has(profile, 'photos') && !_.isEmpty(profile.photos)) {
user.avatarThumb = profile.photos[0].value;
}
console.log('new user', user);
if (!user.username && !user.email) {
throw new Error('Neither a username nor email was available');
}
let dbPassport = await Passport.findOne({
where: {
provider,
identifier: query.identifier.toString(),
},
});
const loginedUser = req.user;
const loginedWithoutDbPassport = !!loginedUser && !dbPassport;
if (loginedWithoutDbPassport) {
// 有一般使用者登入但沒使用FB註冊過
sails.log('=== loginedWithoutdbPassport ===');
query.UserId = loginedUser.id;
await Passport.create(query);
return next(null, loginedUser);
}
if (dbPassport) {
// 已用FB註冊過,直接登入
sails.log('=== dbPassport ===');
if (_.has(query, 'tokens') && query.tokens !== passport.tokens) {
dbPassport.tokens = query.tokens;
dbPassport = await dbPassport.save();
}
sails.log('=== dbPassport passport ===', passport);
user = await User.findOne({
where: {
id: dbPassport.UserId,
},
include: [{
model: Role,
include: [Group],
}],
});
if (user) { return next(null, user); }
throw new Error('Error user not found');
} else {
// 全新使用者沒有 user 也沒有 password
let checkMail;
if (_.has(user, 'eamil')) {
checkMail = await User.findOne({ where: { email: user.email } });
}
if (checkMail) {
throw new Error('Error passport email exists');
}
let newUser = await User.create(user);
query.UserId = newUser.id;
await Passport.createDefaultLocalProviderIfNotExist(newUser);
newUser = await User.findOne({
where: {
id: newUser.id,
},
include: [{
model: Role,
include: [Group],
}],
});
const newFacebookPassword = await Passport.create(query);
// 寄送歡迎註冊信
const languageCode = req.getLocale() || 'zh-TW';
const userMailConfig = await MessageService.greeting({
email: newUser.email || '',
user: newUser,
languageCode,
});
const userEmail = await Notification.create(userMailConfig);
await MessageService.sendMail(userEmail);
// 寄送信箱認證信
// 如果有信箱
if (newUser.email) {
newUser.verificationEmailToken = crypto.randomBytes(32).toString('hex').substr(0, 32);
await newUser.save();
await UserService.sendVerificationEmail({
userId: newUser.id,
email: newUser.email,
displayName: newUser.displayName,
signToken: newUser.verificationEmailToken,
type: '註冊',
languageCode,
});
}
return next(null, newUser);
}
} catch (err) {
sails.log.error(err.stack);
return next(err);
}
};
/**
* Create an authentication endpoint
#
* For more information on authentication in Passport.js, check out:
* http://passportjs.org/guide/authenticate/
#
* @param {Object} req
* @param {Object} res
*/
passport.endpoint = function (req, res) {
let options;
let provider;
let strategies;
strategies = sails.config.strategies;
provider = req.param('provider');
options = {};
if (!strategies.hasOwnProperty(provider)) {
return res.redirect('/login');
}
if (strategies[provider].hasOwnProperty('scope')) {
options.scope = strategies[provider].scope;
}
return this.authenticate(provider, options)(req, res, req.next);
};
/**
* Create an authentication callback endpoint
#
* For more information on authentication in Passport.js, check out:
* http://passportjs.org/guide/authenticate/
#
* @param {Object} req
* @param {Object} res
* @param {Function} next
*/
passport.callback = async function callback({
model,
input,
protocol,
action,
}, req, res, next) {
action = action || req.param('action');
const provider = req.param('provider', 'local');
sails.log.info('=== start login...');
sails.log.info('=== provider ===>', provider);
sails.log.info('=== action ===>', action);
try {
if (provider === 'local' && action) {
switch (action) {
case 'register': {
if (req.user) {
sails.log('====================================');
sails.log('logged user=>', req.user.username);
sails.log('====================================');
throw Error(MESSAGE.BAD_REQUEST.USER_ALREADY_LOGIN);
// req.session.authenticated = false;
// req.logout();
}
try {
if (!_.isNil(protocol) && this.protocols[protocol]) {
return await this.protocols[protocol].register(input, req, res, next);
}
return await this.protocols.local.register(model, req, res, next);
} catch (e) {
sails.log.error('register passport callback error=>', e.stack);
throw e;
}
}
case 'connect': {
if (req.user) {
return this.protocols.local.connect(req, res, next);
}
break;
}
case 'disconnect': {
if (req.user) {
return this.protocols.local.disconnect(req, res, next);
}
break;
}
default: throw new Error('Passport.Local.Action.Invalid');
}
} else if (action === 'disconnect' && req.user) {
return this.disconnect(req, res, next);
}
sails.log.info('=== start authenticate...');
return this.authenticate(provider, next)(req, res, req.next);
} catch (e) {
sails.log.error(e.stack);
return next(e);
}
};
/**
* Load all strategies defined in the Passport configuration
#
* For example, we could add this to our config to use the GitHub strategy
* with permission to access a users email address (even if it's marked as
* private) as well as permission to add and update a user's Gists:
#
github: {
name: 'GitHub',
protocol: 'oauth2',
strategy: require('passport-github').Strategy
scope: [ 'user', 'gist' ]
options: {
clientID: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET'
}
}
#
* For more information on the providers supported by Passport.js, check out:
* http://passportjs.org/guide/providers/
#
*/
passport.loadStrategies = function () {
sails.log('Loading Strategies...');
let self;
let strategies;
self = this;
strategies = sails.config.strategies;
Object.keys(strategies).forEach((key) => {
let Strategy;
let baseUrl;
let callback;
let options;
let protocol;
options = {
passReqToCallback: true,
};
Strategy = void 0;
if (key === 'local') {
_.extend(options, {
usernameField: 'identifier',
});
_.extend(options, strategies[key].options || {});
if (strategies.local) {
Strategy = strategies[key].strategy;
self.use(new Strategy(options, self.protocols.local.login));
}
} else if (key === 'bearer') {
if (strategies.bearer) {
Strategy = strategies[key].strategy;
self.use(new Strategy(self.protocols.bearer.authorize));
}
} else {
const isIdEmpty = strategies[key].options.clientID === '';
const isIdUndefined = strategies[key].options.clientID === undefined;
if (!isIdEmpty && !isIdUndefined) {
protocol = strategies[key].protocol;
callback = strategies[key].callback;
if (!callback) {
callback = `auth/${key}/callback`;
}
Strategy = strategies[key].strategy;
baseUrl = ConfigHelper.getBaseUrl();
switch (protocol) {
case 'oauth':
break;
case 'oauth2':
options.callbackURL = url.resolve(baseUrl, callback);
break;
case 'openid':
options.returnURL = url.resolve(baseUrl, callback);
options.realm = baseUrl;
options.profile = true;
break;
default:
break;
}
_.extend(options, strategies[key].options);
self.use(new Strategy(options, self.protocols[protocol]));
}
}
});
};
/**
* Disconnect a passport from a user
#
* @param {Object} req
* @param {Object} res
*/
passport.disconnect = function (req, res, next) {
const provider = req.param('provider');
const user = req.user;
return Passport.findOne({
where: {
provider,
user: user.id,
},
}).then(p => Passport.destroy(p.id).then(() => next(null, user)));
};
passport.serializeUser((user, next) => {
console.log('serializeUser user=>', user);
return next(null, user)
});
passport.deserializeUser((user, next) => next(null, user));
module.exports = passport;