chore: Remove old auth cruft
This commit is contained in:
committed by
mrugesh mohapatra
parent
081284c2d3
commit
0c23844793
@ -11,24 +11,24 @@ import moment from 'moment';
|
|||||||
import dedent from 'dedent';
|
import dedent from 'dedent';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
import { isEmail } from 'validator';
|
import { isEmail } from 'validator';
|
||||||
import path from 'path';
|
|
||||||
import loopback from 'loopback';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
import generate from 'nanoid/generate';
|
import generate from 'nanoid/generate';
|
||||||
|
|
||||||
import { homeLocation, apiLocation } from '../../../config/env';
|
import { apiLocation } from '../../../config/env';
|
||||||
|
|
||||||
import { fixCompletedChallengeItem } from '../utils';
|
import { fixCompletedChallengeItem } from '../utils';
|
||||||
import { saveUser, observeMethod } from '../../server/utils/rx.js';
|
import { saveUser, observeMethod } from '../../server/utils/rx.js';
|
||||||
import { blacklistedUsernames } from '../../server/utils/constants.js';
|
import { blacklistedUsernames } from '../../server/utils/constants.js';
|
||||||
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
|
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
|
||||||
import { getEmailSender } from '../../server/utils/url-utils.js';
|
|
||||||
import {
|
import {
|
||||||
normaliseUserFields,
|
normaliseUserFields,
|
||||||
getProgress,
|
getProgress,
|
||||||
publicUserProps
|
publicUserProps
|
||||||
} from '../../server/utils/publicUserProps';
|
} from '../../server/utils/publicUserProps';
|
||||||
|
import {
|
||||||
|
setAccessTokenToResponse,
|
||||||
|
removeCookies
|
||||||
|
} from '../../server/utils/getSetAccessToken';
|
||||||
|
|
||||||
const log = debugFactory('fcc:models:user');
|
const log = debugFactory('fcc:models:user');
|
||||||
const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
|
const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
|
||||||
@ -93,42 +93,6 @@ function isTheSame(val1, val2) {
|
|||||||
return val1 === val2;
|
return val1 === val2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderSignUpEmail = loopback.template(
|
|
||||||
path.join(
|
|
||||||
__dirname,
|
|
||||||
'..',
|
|
||||||
'..',
|
|
||||||
'server',
|
|
||||||
'views',
|
|
||||||
'emails',
|
|
||||||
'user-request-sign-up.ejs'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderSignInEmail = loopback.template(
|
|
||||||
path.join(
|
|
||||||
__dirname,
|
|
||||||
'..',
|
|
||||||
'..',
|
|
||||||
'server',
|
|
||||||
'views',
|
|
||||||
'emails',
|
|
||||||
'user-request-sign-in.ejs'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderEmailChangeEmail = loopback.template(
|
|
||||||
path.join(
|
|
||||||
__dirname,
|
|
||||||
'..',
|
|
||||||
'..',
|
|
||||||
'server',
|
|
||||||
'views',
|
|
||||||
'emails',
|
|
||||||
'user-request-update-email.ejs'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
function getAboutProfile({
|
function getAboutProfile({
|
||||||
username,
|
username,
|
||||||
githubProfile: github,
|
githubProfile: github,
|
||||||
@ -147,37 +111,34 @@ function nextTick(fn) {
|
|||||||
return process.nextTick(fn);
|
return process.nextTick(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWaitPeriod(ttl) {
|
|
||||||
const fiveMinutesAgo = moment().subtract(5, 'minutes');
|
|
||||||
const lastEmailSentAt = moment(new Date(ttl || null));
|
|
||||||
const isWaitPeriodOver = ttl
|
|
||||||
? lastEmailSentAt.isBefore(fiveMinutesAgo)
|
|
||||||
: true;
|
|
||||||
|
|
||||||
if (!isWaitPeriodOver) {
|
|
||||||
const minutesLeft = 5 - (moment().minutes() - lastEmailSentAt.minutes());
|
|
||||||
return minutesLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWaitMessage(ttl) {
|
|
||||||
const minutesLeft = getWaitPeriod(ttl);
|
|
||||||
if (minutesLeft <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const timeToWait = minutesLeft
|
|
||||||
? `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}`
|
|
||||||
: 'a few seconds';
|
|
||||||
|
|
||||||
return dedent`
|
|
||||||
Please wait ${timeToWait} to resend an authentication link.
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
const getRandomNumber = () => Math.random();
|
const getRandomNumber = () => Math.random();
|
||||||
|
|
||||||
module.exports = function(User) {
|
function populateRequiredFields(user) {
|
||||||
|
user.username = user.username.trim().toLowerCase();
|
||||||
|
user.email =
|
||||||
|
typeof user.email === 'string'
|
||||||
|
? user.email.trim().toLowerCase()
|
||||||
|
: user.email;
|
||||||
|
|
||||||
|
if (!user.progressTimestamps) {
|
||||||
|
user.progressTimestamps = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.progressTimestamps.length === 0) {
|
||||||
|
user.progressTimestamps.push(Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.externalId) {
|
||||||
|
user.externalId = uuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.unsubscribeId) {
|
||||||
|
user.unsubscribeId = generate(nanoidCharSet, 20);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(User) {
|
||||||
// set salt factor for passwords
|
// set salt factor for passwords
|
||||||
User.settings.saltWorkFactor = 5;
|
User.settings.saltWorkFactor = 5;
|
||||||
// set user.rand to random number
|
// set user.rand to random number
|
||||||
@ -217,29 +178,13 @@ module.exports = function(User) {
|
|||||||
throw createEmailError();
|
throw createEmailError();
|
||||||
}
|
}
|
||||||
// assign random username to new users
|
// assign random username to new users
|
||||||
// actual usernames will come from github
|
|
||||||
// use full uuid to ensure uniqueness
|
|
||||||
user.username = 'fcc' + uuid();
|
user.username = 'fcc' + uuid();
|
||||||
|
populateRequiredFields(user);
|
||||||
if (!user.externalId) {
|
|
||||||
user.externalId = uuid();
|
|
||||||
}
|
|
||||||
if (!user.unsubscribeId) {
|
|
||||||
user.unsubscribeId = generate(nanoidCharSet, 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.progressTimestamps) {
|
|
||||||
user.progressTimestamps = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.progressTimestamps.length === 0) {
|
|
||||||
user.progressTimestamps.push(Date.now());
|
|
||||||
}
|
|
||||||
return Observable.fromPromise(User.doesExist(null, user.email)).do(
|
return Observable.fromPromise(User.doesExist(null, user.email)).do(
|
||||||
exists => {
|
exists => {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
throw wrapHandledError(new Error('user already exists'), {
|
throw wrapHandledError(new Error('user already exists'), {
|
||||||
redirectTo: `${homeLocation}/signin`,
|
redirectTo: `${apiLocation}/signin`,
|
||||||
message: dedent`
|
message: dedent`
|
||||||
The ${user.email} email address is already associated with an account.
|
The ${user.email} email address is already associated with an account.
|
||||||
Try signing in with it here instead.
|
Try signing in with it here instead.
|
||||||
@ -263,28 +208,7 @@ module.exports = function(User) {
|
|||||||
if (user.email && !isEmail(user.email)) {
|
if (user.email && !isEmail(user.email)) {
|
||||||
throw createEmailError();
|
throw createEmailError();
|
||||||
}
|
}
|
||||||
|
populateRequiredFields(user);
|
||||||
user.username = user.username.trim().toLowerCase();
|
|
||||||
user.email =
|
|
||||||
typeof user.email === 'string'
|
|
||||||
? user.email.trim().toLowerCase()
|
|
||||||
: user.email;
|
|
||||||
|
|
||||||
if (!user.progressTimestamps) {
|
|
||||||
user.progressTimestamps = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.progressTimestamps.length === 0) {
|
|
||||||
user.progressTimestamps.push(Date.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.externalId) {
|
|
||||||
user.externalId = uuid();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.unsubscribeId) {
|
|
||||||
user.unsubscribeId = generate(nanoidCharSet, 20);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.ignoreElements();
|
.ignoreElements();
|
||||||
return Observable.merge(beforeCreate, updateOrSave).toPromise();
|
return Observable.merge(beforeCreate, updateOrSave).toPromise();
|
||||||
@ -364,32 +288,13 @@ module.exports = function(User) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function manualReload() {
|
|
||||||
this.reload((err, instance) => {
|
|
||||||
if (err) {
|
|
||||||
throw Error('failed to reload user instance');
|
|
||||||
}
|
|
||||||
Object.assign(this, instance);
|
|
||||||
log('user reloaded from db');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
User.prototype.manualReload = manualReload;
|
|
||||||
|
|
||||||
User.prototype.loginByRequest = function loginByRequest(req, res) {
|
User.prototype.loginByRequest = function loginByRequest(req, res) {
|
||||||
const {
|
const {
|
||||||
query: { emailChange }
|
query: { emailChange }
|
||||||
} = req;
|
} = req;
|
||||||
const createToken = this.createAccessToken$().do(accessToken => {
|
const createToken = this.createAccessToken$().do(accessToken => {
|
||||||
const config = {
|
|
||||||
signed: !!req.signedCookies,
|
|
||||||
maxAge: accessToken.ttl,
|
|
||||||
domain: process.env.COOKIE_DOMAIN || 'localhost'
|
|
||||||
};
|
|
||||||
if (accessToken && accessToken.id) {
|
if (accessToken && accessToken.id) {
|
||||||
const jwtAccess = jwt.sign({ accessToken }, process.env.JWT_SECRET);
|
setAccessTokenToResponse({ accessToken }, req, res);
|
||||||
res.cookie('jwt_access_token', jwtAccess, config);
|
|
||||||
res.cookie('access_token', accessToken.id, config);
|
|
||||||
res.cookie('userId', accessToken.userId, config);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let data = {
|
let data = {
|
||||||
@ -421,14 +326,7 @@ module.exports = function(User) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
User.afterRemote('logout', function({ req, res }, result, next) {
|
User.afterRemote('logout', function({ req, res }, result, next) {
|
||||||
const config = {
|
removeCookies(req, res);
|
||||||
signed: !!req.signedCookies,
|
|
||||||
domain: process.env.COOKIE_DOMAIN || 'localhost'
|
|
||||||
};
|
|
||||||
res.clearCookie('jwt_access_token', config);
|
|
||||||
res.clearCookie('access_token', config);
|
|
||||||
res.clearCookie('userId', config);
|
|
||||||
res.clearCookie('_csrf', config);
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -533,154 +431,6 @@ module.exports = function(User) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
User.prototype.getEncodedEmail = function getEncodedEmail(email) {
|
|
||||||
if (!email) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Buffer(email).toString('base64');
|
|
||||||
};
|
|
||||||
|
|
||||||
User.decodeEmail = email => Buffer(email, 'base64').toString();
|
|
||||||
|
|
||||||
function requestAuthEmail(isSignUp, newEmail) {
|
|
||||||
return Observable.defer(() => {
|
|
||||||
const messageOrNull = getWaitMessage(this.emailAuthLinkTTL);
|
|
||||||
if (messageOrNull) {
|
|
||||||
throw wrapHandledError(new Error('request is throttled'), {
|
|
||||||
type: 'info',
|
|
||||||
message: messageOrNull
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a temporary access token with ttl for 15 minutes
|
|
||||||
return this.createAuthToken({ ttl: 15 * 60 * 1000 });
|
|
||||||
})
|
|
||||||
.flatMap(token => {
|
|
||||||
let renderAuthEmail = renderSignInEmail;
|
|
||||||
let subject = 'Your sign in link for freeCodeCamp.org';
|
|
||||||
if (isSignUp) {
|
|
||||||
renderAuthEmail = renderSignUpEmail;
|
|
||||||
subject = 'Your sign in link for your new freeCodeCamp.org account';
|
|
||||||
}
|
|
||||||
if (newEmail) {
|
|
||||||
renderAuthEmail = renderEmailChangeEmail;
|
|
||||||
subject = dedent`
|
|
||||||
Please confirm your updated email address for freeCodeCamp.org
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
const { id: loginToken, created: emailAuthLinkTTL } = token;
|
|
||||||
const loginEmail = this.getEncodedEmail(newEmail ? newEmail : null);
|
|
||||||
const host = apiLocation;
|
|
||||||
const mailOptions = {
|
|
||||||
type: 'email',
|
|
||||||
to: newEmail ? newEmail : this.email,
|
|
||||||
from: getEmailSender(),
|
|
||||||
subject,
|
|
||||||
text: renderAuthEmail({
|
|
||||||
host,
|
|
||||||
loginEmail,
|
|
||||||
loginToken,
|
|
||||||
emailChange: !!newEmail
|
|
||||||
})
|
|
||||||
};
|
|
||||||
const userUpdate = new Promise((resolve, reject) =>
|
|
||||||
this.updateAttributes({ emailAuthLinkTTL }, err => {
|
|
||||||
if (err) {
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
return resolve();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return Observable.forkJoin(
|
|
||||||
User.email.send$(mailOptions),
|
|
||||||
Observable.fromPromise(userUpdate)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map(
|
|
||||||
() =>
|
|
||||||
'Check your email and click the link we sent you to confirm' +
|
|
||||||
' your new email address.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
User.prototype.requestAuthEmail = requestAuthEmail;
|
|
||||||
|
|
||||||
User.prototype.requestUpdateEmail = function requestUpdateEmail(newEmail) {
|
|
||||||
const currentEmail = this.email;
|
|
||||||
const isOwnEmail = isTheSame(newEmail, currentEmail);
|
|
||||||
const isResendUpdateToSameEmail = isTheSame(newEmail, this.newEmail);
|
|
||||||
const isLinkSentWithinLimit = getWaitMessage(this.emailVerifyTTL);
|
|
||||||
const isVerifiedEmail = this.emailVerified;
|
|
||||||
|
|
||||||
if (isOwnEmail && isVerifiedEmail) {
|
|
||||||
// email is already associated and verified with this account
|
|
||||||
throw wrapHandledError(new Error('email is already verified'), {
|
|
||||||
type: 'info',
|
|
||||||
message: `
|
|
||||||
${newEmail} is already associated with this account.
|
|
||||||
You can update a new email address instead.`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (isResendUpdateToSameEmail && isLinkSentWithinLimit) {
|
|
||||||
// trying to update with the same newEmail and
|
|
||||||
// confirmation email is still valid
|
|
||||||
throw wrapHandledError(new Error(), {
|
|
||||||
type: 'info',
|
|
||||||
message: dedent`
|
|
||||||
We have already sent an email confirmation request to ${newEmail}.
|
|
||||||
${isLinkSentWithinLimit}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!isEmail('' + newEmail)) {
|
|
||||||
throw createEmailError();
|
|
||||||
}
|
|
||||||
|
|
||||||
// newEmail is not associated with this user, and
|
|
||||||
// this attempt to change email is the first or
|
|
||||||
// previous attempts have expired
|
|
||||||
if (
|
|
||||||
!isOwnEmail ||
|
|
||||||
(isOwnEmail && !isVerifiedEmail) ||
|
|
||||||
(isResendUpdateToSameEmail && !isLinkSentWithinLimit)
|
|
||||||
) {
|
|
||||||
const updateConfig = {
|
|
||||||
newEmail,
|
|
||||||
emailVerified: false,
|
|
||||||
emailVerifyTTL: new Date()
|
|
||||||
};
|
|
||||||
|
|
||||||
// defer prevents the promise from firing prematurely (before subscribe)
|
|
||||||
return Observable.defer(() => User.doesExist(null, newEmail))
|
|
||||||
.do(exists => {
|
|
||||||
if (exists && !isOwnEmail) {
|
|
||||||
// newEmail is not associated with this account,
|
|
||||||
// but is associated with different account
|
|
||||||
throw wrapHandledError(new Error('email already in use'), {
|
|
||||||
type: 'info',
|
|
||||||
message: `${newEmail} is already associated with another account.`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.flatMap(() => {
|
|
||||||
const updatePromise = new Promise((resolve, reject) =>
|
|
||||||
this.updateAttributes(updateConfig, err => {
|
|
||||||
if (err) {
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
return resolve();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return Observable.forkJoin(
|
|
||||||
Observable.fromPromise(updatePromise),
|
|
||||||
this.requestAuthEmail(false, newEmail),
|
|
||||||
(_, message) => message
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return 'Something unexpected happened while updating your email.';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function requestCompletedChallenges() {
|
function requestCompletedChallenges() {
|
||||||
return this.getCompletedChallenges$();
|
return this.getCompletedChallenges$();
|
||||||
}
|
}
|
||||||
@ -1117,4 +867,4 @@ module.exports = function(User) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { Observable } from 'rx';
|
|
||||||
import dedent from 'dedent';
|
|
||||||
import passport from 'passport';
|
import passport from 'passport';
|
||||||
import { isEmail } from 'validator';
|
|
||||||
import { check } from 'express-validator/check';
|
|
||||||
|
|
||||||
import { homeLocation } from '../../../config/env';
|
import { homeLocation } from '../../../config/env';
|
||||||
import {
|
import {
|
||||||
@ -11,11 +6,7 @@ import {
|
|||||||
saveResponseAuthCookies,
|
saveResponseAuthCookies,
|
||||||
loginRedirect
|
loginRedirect
|
||||||
} from '../component-passport';
|
} from '../component-passport';
|
||||||
import {
|
import { ifUserRedirectTo } from '../utils/middleware';
|
||||||
ifUserRedirectTo,
|
|
||||||
ifNoUserRedirectTo,
|
|
||||||
createValidatorErrorHandler
|
|
||||||
} from '../utils/middleware';
|
|
||||||
import { wrapHandledError } from '../utils/create-handled-error.js';
|
import { wrapHandledError } from '../utils/create-handled-error.js';
|
||||||
import { removeCookies } from '../utils/getSetAccessToken';
|
import { removeCookies } from '../utils/getSetAccessToken';
|
||||||
|
|
||||||
@ -31,9 +22,7 @@ module.exports = function enableAuthentication(app) {
|
|||||||
const ifUserRedirect = ifUserRedirectTo();
|
const ifUserRedirect = ifUserRedirectTo();
|
||||||
const saveAuthCookies = saveResponseAuthCookies();
|
const saveAuthCookies = saveResponseAuthCookies();
|
||||||
const loginSuccessRedirect = loginRedirect();
|
const loginSuccessRedirect = loginRedirect();
|
||||||
const ifNoUserRedirectHome = ifNoUserRedirectTo(homeLocation);
|
|
||||||
const api = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
const { AuthToken, User } = app.models;
|
|
||||||
|
|
||||||
// Use a local mock strategy for signing in if we are in dev mode.
|
// Use a local mock strategy for signing in if we are in dev mode.
|
||||||
// Otherwise we use auth0 login. We use a string for 'true' because values
|
// Otherwise we use auth0 login. We use a string for 'true' because values
|
||||||
@ -73,167 +62,5 @@ module.exports = function enableAuthentication(app) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultErrorMsg = dedent`
|
|
||||||
Oops, something is not right,
|
|
||||||
please request a fresh link to sign in / sign up.
|
|
||||||
`;
|
|
||||||
|
|
||||||
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.')
|
|
||||||
];
|
|
||||||
|
|
||||||
function getPasswordlessAuth(req, res, next) {
|
|
||||||
const {
|
|
||||||
query: { email: encodedEmail, token: authTokenId, emailChange } = {}
|
|
||||||
} = req;
|
|
||||||
|
|
||||||
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: `${homeLocation}/signin`
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// first find
|
|
||||||
return (
|
|
||||||
AuthToken.findOne$({ where: { id: authTokenId } })
|
|
||||||
.flatMap(authToken => {
|
|
||||||
if (!authToken) {
|
|
||||||
throw wrapHandledError(
|
|
||||||
new Error(`no token found for id: ${authTokenId}`),
|
|
||||||
{
|
|
||||||
type: 'info',
|
|
||||||
message: defaultErrorMsg,
|
|
||||||
redirectTo: `${homeLocation}/signin`
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// find user then validate and destroy email validation token
|
|
||||||
// finally retun user instance
|
|
||||||
return User.findOne$({ where: { id: authToken.userId } }).flatMap(
|
|
||||||
user => {
|
|
||||||
if (!user) {
|
|
||||||
throw wrapHandledError(
|
|
||||||
new Error(`no user found for token: ${authTokenId}`),
|
|
||||||
{
|
|
||||||
type: 'info',
|
|
||||||
message: defaultErrorMsg,
|
|
||||||
redirectTo: `${homeLocation}/signin`
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (user.email !== email) {
|
|
||||||
if (!emailChange || (emailChange && user.newEmail !== email)) {
|
|
||||||
throw wrapHandledError(
|
|
||||||
new Error('user email does not match'),
|
|
||||||
{
|
|
||||||
type: 'info',
|
|
||||||
message: defaultErrorMsg,
|
|
||||||
redirectTo: `${homeLocation}/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: `${homeLocation}/signin`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return authToken.destroy$();
|
|
||||||
})
|
|
||||||
.map(() => user);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
// at this point token has been validated and destroyed
|
|
||||||
// update user and log them in
|
|
||||||
.map(user => user.loginByRequest(req, res))
|
|
||||||
.do(() => {
|
|
||||||
req.flash(
|
|
||||||
'success',
|
|
||||||
'Success! You have signed in to your account. Happy Coding!'
|
|
||||||
);
|
|
||||||
return res.redirectWithFlash(`${homeLocation}/welcome`);
|
|
||||||
})
|
|
||||||
.subscribe(() => {}, next)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
api.get(
|
|
||||||
'/passwordless-auth',
|
|
||||||
ifUserRedirect,
|
|
||||||
passwordlessGetValidators,
|
|
||||||
createValidatorErrorHandler('errors', `${homeLocation}/signin`),
|
|
||||||
getPasswordlessAuth
|
|
||||||
);
|
|
||||||
|
|
||||||
api.get('/passwordless-change', (req, res) =>
|
|
||||||
res.redirect(301, '/confirm-email')
|
|
||||||
);
|
|
||||||
|
|
||||||
api.get(
|
|
||||||
'/confirm-email',
|
|
||||||
ifNoUserRedirectHome,
|
|
||||||
passwordlessGetValidators,
|
|
||||||
getPasswordlessAuth
|
|
||||||
);
|
|
||||||
|
|
||||||
const passwordlessPostValidators = [
|
|
||||||
check('email')
|
|
||||||
.isEmail()
|
|
||||||
.withMessage('Email is not a valid email address.')
|
|
||||||
];
|
|
||||||
function postPasswordlessAuth(req, res, next) {
|
|
||||||
const { body: { email } = {} } = req;
|
|
||||||
|
|
||||||
return User.findOne$({ where: { email } })
|
|
||||||
.flatMap(_user =>
|
|
||||||
Observable.if(
|
|
||||||
// if no user found create new user and save to db
|
|
||||||
_.constant(_user),
|
|
||||||
Observable.of(_user),
|
|
||||||
User.create$({ email })
|
|
||||||
).flatMap(user => user.requestAuthEmail(!_user))
|
|
||||||
)
|
|
||||||
.do(msg => {
|
|
||||||
let redirectTo = homeLocation;
|
|
||||||
|
|
||||||
if (req.session && req.session.returnTo) {
|
|
||||||
redirectTo = req.session.returnTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
req.flash('info', msg);
|
|
||||||
return res.redirect(redirectTo);
|
|
||||||
})
|
|
||||||
.subscribe(_.noop, next);
|
|
||||||
}
|
|
||||||
|
|
||||||
api.post(
|
|
||||||
'/passwordless-auth',
|
|
||||||
ifUserRedirect,
|
|
||||||
passwordlessPostValidators,
|
|
||||||
createValidatorErrorHandler('errors', `${homeLocation}/signin`),
|
|
||||||
postPasswordlessAuth
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(api);
|
app.use(api);
|
||||||
};
|
};
|
||||||
|
@ -25,8 +25,6 @@ export function setAccessTokenToResponse(
|
|||||||
};
|
};
|
||||||
const jwtAccess = jwt.sign({ accessToken }, jwtSecret);
|
const jwtAccess = jwt.sign({ accessToken }, jwtSecret);
|
||||||
res.cookie(jwtCookieNS, jwtAccess, cookieConfig);
|
res.cookie(jwtCookieNS, jwtAccess, cookieConfig);
|
||||||
res.cookie('access_token', accessToken.id, cookieConfig);
|
|
||||||
res.cookie('userId', accessToken.userId, cookieConfig);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,8 +121,7 @@ describe('getSetAccessToken', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('setAccessTokenToResponse', () => {
|
describe('setAccessTokenToResponse', () => {
|
||||||
it('sets three cookies in the response', () => {
|
it('sets a jwt access token cookie in the response', () => {
|
||||||
expect.assertions(3);
|
|
||||||
const req = mockReq();
|
const req = mockReq();
|
||||||
const res = mockRes();
|
const res = mockRes();
|
||||||
|
|
||||||
@ -139,24 +138,6 @@ describe('getSetAccessToken', () => {
|
|||||||
maxAge: accessToken.ttl
|
maxAge: accessToken.ttl
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
expect(res.cookie.getCall(1).args).toEqual([
|
|
||||||
'access_token',
|
|
||||||
accessToken.id,
|
|
||||||
{
|
|
||||||
signed: false,
|
|
||||||
domain: 'localhost',
|
|
||||||
maxAge: accessToken.ttl
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
expect(res.cookie.getCall(2).args).toEqual([
|
|
||||||
'userId',
|
|
||||||
accessToken.userId,
|
|
||||||
{
|
|
||||||
signed: false,
|
|
||||||
domain: 'localhost',
|
|
||||||
maxAge: accessToken.ttl
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
Here's your sign in link. It will instantly sign you into freeCodeCamp.org - no password necessary:
|
|
||||||
|
|
||||||
<%= host %>/internal/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %>
|
|
||||||
|
|
||||||
Note: this sign in link will expire after 15 minutes. If you need a new sign in link, go to https://www.freecodecamp.org/signin
|
|
||||||
|
|
||||||
See you soon!
|
|
||||||
|
|
||||||
- The freeCodeCamp.org Team
|
|
@ -1,13 +0,0 @@
|
|||||||
Welcome to the freeCodeCamp community!
|
|
||||||
|
|
||||||
We have created a new account for you.
|
|
||||||
|
|
||||||
Here's your sign in link. It will instantly sign you into freeCodeCamp.org - no password necessary:
|
|
||||||
|
|
||||||
<%= host %>/internal/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %>
|
|
||||||
|
|
||||||
Note: this sign in link will expire after 15 minutes. If you need a new sign in link, go to https://www.freecodecamp.org/signin
|
|
||||||
|
|
||||||
See you soon!
|
|
||||||
|
|
||||||
- The freeCodeCamp.org Team
|
|
@ -1,7 +0,0 @@
|
|||||||
Please confirm this address for freeCodeCamp:
|
|
||||||
|
|
||||||
<%= host %>/internal/confirm-email?email=<%= loginEmail %>&token=<%= loginToken %>&emailChange=<%= emailChange %>
|
|
||||||
|
|
||||||
Happy coding!
|
|
||||||
|
|
||||||
- The freeCodeCamp.org Team
|
|
Reference in New Issue
Block a user