2017-12-27 10:11:17 -08:00
|
|
|
import _ from 'lodash';
|
|
|
|
import { Observable } from 'rx';
|
2017-12-26 13:20:03 -08:00
|
|
|
import dedent from 'dedent';
|
2017-12-27 10:11:17 -08:00
|
|
|
// import debugFactory from 'debug';
|
|
|
|
import { isEmail } from 'validator';
|
2017-12-27 12:16:47 -08:00
|
|
|
import { check, validationResult } from 'express-validator/check';
|
2017-12-27 10:11:17 -08:00
|
|
|
|
|
|
|
import { ifUserRedirectTo } from '../utils/middleware';
|
|
|
|
import {
|
|
|
|
wrapHandledError,
|
|
|
|
createValidatorErrorFormatter
|
|
|
|
} from '../utils/create-handled-error.js';
|
2017-12-26 13:20:03 -08:00
|
|
|
|
|
|
|
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
|
2017-12-27 10:11:17 -08:00
|
|
|
// const debug = debugFactory('fcc:boot:auth');
|
2017-12-26 13:20:03 -08:00
|
|
|
|
2015-06-03 12:26:11 -07:00
|
|
|
module.exports = function enableAuthentication(app) {
|
2017-12-26 13:20:03 -08:00
|
|
|
// enable loopback access control authentication. see:
|
|
|
|
// loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html
|
2015-06-03 12:26:11 -07:00
|
|
|
app.enableAuth();
|
2017-12-27 10:11:17 -08:00
|
|
|
const ifUserRedirect = ifUserRedirectTo();
|
2017-12-26 13:20:03 -08:00
|
|
|
const router = app.loopback.Router();
|
|
|
|
const api = app.loopback.Router();
|
|
|
|
const { AccessToken, User } = app.models;
|
|
|
|
|
2017-12-27 10:11:17 -08:00
|
|
|
router.get('/login', (req, res) => res.redirect(301, '/signin'));
|
|
|
|
router.get('/logout', (req, res) => res.redirect(301, '/signout'));
|
2017-12-26 13:20:03 -08:00
|
|
|
|
|
|
|
function getEmailSignin(req, res) {
|
|
|
|
if (isSignUpDisabled) {
|
|
|
|
return res.render('account/beta', {
|
|
|
|
title: 'New sign ups are disabled'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return res.render('account/email-signin', {
|
|
|
|
title: 'Sign in to freeCodeCamp using your Email Address'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-12-27 10:11:17 -08:00
|
|
|
router.get('/signup', ifUserRedirect, getEmailSignin);
|
|
|
|
router.get('/signin', ifUserRedirect, getEmailSignin);
|
|
|
|
router.get('/email-signin', ifUserRedirect, getEmailSignin);
|
2017-12-26 13:20:03 -08:00
|
|
|
|
2017-12-27 10:11:17 -08:00
|
|
|
router.get('/signout', (req, res) => {
|
2017-12-26 13:20:03 -08:00
|
|
|
req.logout();
|
|
|
|
res.redirect('/');
|
2017-12-27 10:11:17 -08:00
|
|
|
});
|
2017-12-26 13:20:03 -08:00
|
|
|
|
|
|
|
|
2017-12-27 10:11:17 -08:00
|
|
|
router.get(
|
|
|
|
'/deprecated-signin',
|
|
|
|
ifUserRedirect,
|
|
|
|
(req, res) => res.render('account/deprecated-signin', {
|
2017-12-26 13:20:03 -08:00
|
|
|
title: 'Sign in to freeCodeCamp using a Deprecated Login'
|
2017-12-27 10:11:17 -08:00
|
|
|
})
|
|
|
|
);
|
2017-12-26 13:20:03 -08:00
|
|
|
|
|
|
|
const defaultErrorMsg = dedent`
|
|
|
|
Oops, something is not right,
|
|
|
|
please request a fresh link to sign in / sign up.
|
|
|
|
`;
|
|
|
|
|
2017-12-27 10:11:17 -08:00
|
|
|
const passwordlessGetValidators = [
|
|
|
|
check('email')
|
|
|
|
.isBase64()
|
|
|
|
.withMessage('email should be a base64 encoded string'),
|
|
|
|
check('token')
|
|
|
|
.exists()
|
|
|
|
.withMessage('token should exist')
|
|
|
|
// based on strongloop/loopback/common/models/access-token.js#L15
|
|
|
|
.isLength({ min: 64, max: 64 })
|
|
|
|
.withMessage('token is not the right length')
|
|
|
|
];
|
|
|
|
|
2017-12-26 13:20:03 -08:00
|
|
|
function getPasswordlessAuth(req, res, next) {
|
2017-12-27 10:11:17 -08:00
|
|
|
const {
|
|
|
|
query: {
|
|
|
|
email: encodedEmail,
|
|
|
|
token: authTokenId
|
|
|
|
} = {}
|
|
|
|
} = req;
|
2017-12-27 12:16:47 -08:00
|
|
|
const validation = validationResult(req)
|
2017-12-27 10:11:17 -08:00
|
|
|
.formatWith(createValidatorErrorFormatter('info', '/email-signup'));
|
|
|
|
|
|
|
|
if (!validation.isEmpty()) {
|
|
|
|
const errors = validation.array();
|
|
|
|
return next(errors.pop());
|
2017-12-26 13:20:03 -08:00
|
|
|
}
|
|
|
|
|
2017-12-27 10:11:17 -08:00
|
|
|
const email = User.decodeEmail(encodedEmail);
|
|
|
|
if (!isEmail(email)) {
|
|
|
|
return next(wrapHandledError(
|
|
|
|
new TypeError('decoded email is invalid'),
|
|
|
|
{
|
|
|
|
type: 'info',
|
|
|
|
message: 'The email encoded in the link is incorrectly formatted',
|
|
|
|
redirectTo: '/email-sign'
|
|
|
|
}
|
|
|
|
));
|
2017-12-26 13:20:03 -08:00
|
|
|
}
|
2017-12-27 10:11:17 -08:00
|
|
|
// first find
|
|
|
|
return AccessToken.findOne$({ where: { id: authTokenId } })
|
|
|
|
.flatMap(authToken => {
|
2017-12-27 17:35:21 -08:00
|
|
|
if (!authToken) {
|
2017-12-27 10:11:17 -08:00
|
|
|
throw wrapHandledError(
|
|
|
|
new Error(`no token found for id: ${authTokenId}`),
|
|
|
|
{
|
|
|
|
type: 'info',
|
|
|
|
message: defaultErrorMsg,
|
|
|
|
redirectTo: '/email-signin'
|
|
|
|
}
|
|
|
|
);
|
2017-12-26 13:20:03 -08:00
|
|
|
}
|
2017-12-27 10:11:17 -08:00
|
|
|
// find user then validate and destroy email validation token
|
|
|
|
// finally retun user instance
|
2017-12-27 17:35:21 -08:00
|
|
|
return User.findOne$({ where: { id: authToken.userId } })
|
2017-12-27 10:11:17 -08:00
|
|
|
.flatMap(user => {
|
|
|
|
if (!user) {
|
|
|
|
throw wrapHandledError(
|
|
|
|
new Error(`no user found for token: ${authTokenId}`),
|
|
|
|
{
|
|
|
|
type: 'info',
|
|
|
|
message: defaultErrorMsg,
|
|
|
|
redirectTo: '/email-signin'
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (user.email !== email) {
|
|
|
|
throw wrapHandledError(
|
|
|
|
new Error('user email does not match'),
|
|
|
|
{
|
|
|
|
type: 'info',
|
|
|
|
message: defaultErrorMsg,
|
|
|
|
redirectTo: '/email-signin'
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return authToken.validate$()
|
|
|
|
.map(isValid => {
|
|
|
|
if (!isValid) {
|
|
|
|
throw wrapHandledError(
|
|
|
|
new Error('token is invalid'),
|
|
|
|
{
|
|
|
|
type: 'info',
|
|
|
|
message: `
|
|
|
|
Looks like the link you clicked has expired,
|
|
|
|
please request a fresh link, to sign in.
|
|
|
|
`,
|
|
|
|
redirectTo: '/email-signin'
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return authToken.destroy$();
|
|
|
|
})
|
|
|
|
.map(() => user);
|
|
|
|
});
|
|
|
|
})
|
|
|
|
// at this point token has been validated and destroyed
|
|
|
|
// update user and log them in
|
|
|
|
.map(user => {
|
2017-12-26 13:20:03 -08:00
|
|
|
const emailVerified = true;
|
|
|
|
const emailAuthLinkTTL = null;
|
|
|
|
const emailVerifyTTL = null;
|
2017-12-27 10:11:17 -08:00
|
|
|
|
|
|
|
const updateUser = user.update$({
|
|
|
|
emailVerified,
|
|
|
|
emailAuthLinkTTL,
|
|
|
|
emailVerifyTTL
|
2017-12-26 13:20:03 -08:00
|
|
|
})
|
2017-12-27 10:11:17 -08:00
|
|
|
.do((user) => {
|
|
|
|
// update$ does not update in place
|
|
|
|
// update user instance to reflect db
|
|
|
|
user.emailVerified = emailVerified;
|
|
|
|
user.emailAuthLinkTTL = emailAuthLinkTTL;
|
|
|
|
user.emailVerifyTTL = emailVerifyTTL;
|
|
|
|
});
|
2017-12-26 13:20:03 -08:00
|
|
|
|
2017-12-27 17:35:21 -08:00
|
|
|
const createToken = user.createAccessToken$()
|
2017-12-27 10:11:17 -08:00
|
|
|
.do(accessToken => {
|
|
|
|
const config = {
|
|
|
|
signed: !!req.signedCookies,
|
|
|
|
maxAge: accessToken.ttl
|
|
|
|
};
|
|
|
|
if (accessToken && accessToken.id) {
|
|
|
|
res.cookie('access_token', accessToken.id, config);
|
|
|
|
res.cookie('userId', accessToken.userId, config);
|
2017-12-26 13:20:03 -08:00
|
|
|
}
|
|
|
|
});
|
2017-12-27 10:11:17 -08:00
|
|
|
|
|
|
|
return Observable.combineLatest(
|
|
|
|
updateUser,
|
|
|
|
createToken,
|
|
|
|
req.logIn(user),
|
|
|
|
);
|
|
|
|
})
|
|
|
|
.do(() => {
|
|
|
|
let redirectTo = '/';
|
|
|
|
|
|
|
|
if (
|
|
|
|
req.session &&
|
|
|
|
req.session.returnTo
|
|
|
|
) {
|
|
|
|
redirectTo = req.session.returnTo;
|
|
|
|
}
|
|
|
|
|
|
|
|
req.flash('success', { msg:
|
|
|
|
'Success! You have signed in to your account. Happy Coding!'
|
2017-12-26 13:20:03 -08:00
|
|
|
});
|
|
|
|
|
2017-12-27 10:11:17 -08:00
|
|
|
return res.redirect(redirectTo);
|
|
|
|
})
|
|
|
|
.subscribe(
|
|
|
|
() => {},
|
|
|
|
next
|
|
|
|
);
|
|
|
|
}
|
2017-12-26 13:20:03 -08:00
|
|
|
|
2017-12-27 10:11:17 -08:00
|
|
|
router.get(
|
|
|
|
'/passwordless-auth',
|
|
|
|
ifUserRedirect,
|
|
|
|
passwordlessGetValidators,
|
|
|
|
getPasswordlessAuth
|
|
|
|
);
|
|
|
|
|
|
|
|
const passwordlessPostValidators = [
|
|
|
|
check('email')
|
|
|
|
.isEmail()
|
|
|
|
.withMessage('email is not a valid email address')
|
|
|
|
];
|
|
|
|
function postPasswordlessAuth(req, res, next) {
|
|
|
|
const { body: { email } = {} } = req;
|
2017-12-27 12:16:47 -08:00
|
|
|
const validation = validationResult(req)
|
2017-12-27 10:11:17 -08:00
|
|
|
.formatWith(createValidatorErrorFormatter('info', '/email-signup'));
|
|
|
|
if (!validation.isEmpty()) {
|
|
|
|
const errors = validation.array();
|
|
|
|
return next(errors.pop());
|
2017-12-26 13:20:03 -08:00
|
|
|
}
|
|
|
|
|
2017-12-27 10:11:17 -08:00
|
|
|
return User.findOne$({ where: { email } })
|
|
|
|
.flatMap(user => (
|
|
|
|
// if no user found create new user and save to db
|
|
|
|
user ? Observable.of(user) : User.create$({ email })
|
|
|
|
))
|
|
|
|
.flatMap(user => user.requestAuthEmail())
|
|
|
|
.do(msg => res.status(200).send({ message: msg }))
|
|
|
|
.subscribe(_.noop, next);
|
2017-12-26 13:20:03 -08:00
|
|
|
}
|
|
|
|
|
2017-12-27 10:11:17 -08:00
|
|
|
api.post(
|
|
|
|
'/passwordless-auth',
|
|
|
|
ifUserRedirect,
|
|
|
|
passwordlessPostValidators,
|
|
|
|
postPasswordlessAuth
|
|
|
|
);
|
2017-12-26 13:20:03 -08:00
|
|
|
|
|
|
|
app.use('/:lang', router);
|
|
|
|
app.use(api);
|
2015-06-02 17:27:02 -07:00
|
|
|
};
|