fix(api): only use homeLocation as a fallback (#40517)

This commit is contained in:
Oliver Eyton-Williams
2020-12-30 20:10:38 +01:00
committed by Mrugesh Mohapatra
parent 03fa21a565
commit a076547d43
22 changed files with 207 additions and 600 deletions

View File

@ -4,7 +4,6 @@ import { check } from 'express-validator';
import { isEmail } from 'validator';
import jwt from 'jsonwebtoken';
import { homeLocation } from '../../../config/env';
import { jwtSecret } from '../../../config/secrets';
import {
@ -12,11 +11,11 @@ import {
devSaveResponseAuthCookies,
devLoginRedirect
} from '../component-passport';
import { ifUserRedirectTo, ifNoUserRedirectTo } from '../utils/middleware';
import { ifUserRedirectTo, ifNoUserRedirectHome } from '../utils/middleware';
import { wrapHandledError } from '../utils/create-handled-error.js';
import { removeCookies } from '../utils/getSetAccessToken';
import { decodeEmail } from '../../common/utils';
import { getParamsFromReq } from '../utils/get-return-to';
import { getRedirectParams } from '../utils/redirection';
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
if (isSignUpDisabled) {
@ -40,7 +39,7 @@ module.exports = function enableAuthentication(app) {
// loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html
app.enableAuth();
const ifUserRedirect = ifUserRedirectTo();
const ifNoUserRedirectHome = ifNoUserRedirectTo(homeLocation);
const ifNoUserRedirect = ifNoUserRedirectHome();
const devSaveAuthCookies = devSaveResponseAuthCookies();
const devLoginSuccessRedirect = devLoginRedirect();
const api = app.loopback.Router();
@ -57,7 +56,7 @@ module.exports = function enableAuthentication(app) {
);
} else {
api.get('/signin', ifUserRedirect, (req, res, next) => {
const { returnTo, origin, pathPrefix } = getParamsFromReq(req);
const { returnTo, origin, pathPrefix } = getRedirectParams(req);
const state = jwt.sign({ returnTo, origin, pathPrefix }, jwtSecret);
return passport.authenticate('auth0-login', { state })(req, res, next);
});
@ -69,23 +68,24 @@ module.exports = function enableAuthentication(app) {
}
api.get('/signout', (req, res) => {
const { origin } = getRedirectParams(req);
req.logout();
req.session.destroy(err => {
if (err) {
throw wrapHandledError(new Error('could not destroy session'), {
type: 'info',
message: 'We could not log you out, please try again in a moment.',
redirectTo: homeLocation
redirectTo: origin
});
}
removeCookies(req, res);
res.redirect(homeLocation);
res.redirect(origin);
});
});
api.get(
'/confirm-email',
ifNoUserRedirectHome,
ifNoUserRedirect,
passwordlessGetValidators,
createGetPasswordlessAuth(app)
);
@ -106,14 +106,14 @@ function createGetPasswordlessAuth(app) {
const {
query: { email: encodedEmail, token: authTokenId, emailChange } = {}
} = req;
const { origin } = getRedirectParams(req);
const email = 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`
redirectTo: `${origin}/signin`
})
);
}
@ -127,7 +127,7 @@ function createGetPasswordlessAuth(app) {
{
type: 'info',
message: defaultErrorMsg,
redirectTo: `${homeLocation}/signin`
redirectTo: `${origin}/signin`
}
);
}
@ -141,7 +141,7 @@ function createGetPasswordlessAuth(app) {
{
type: 'info',
message: defaultErrorMsg,
redirectTo: `${homeLocation}/signin`
redirectTo: `${origin}/signin`
}
);
}
@ -152,7 +152,7 @@ function createGetPasswordlessAuth(app) {
{
type: 'info',
message: defaultErrorMsg,
redirectTo: `${homeLocation}/signin`
redirectTo: `${origin}/signin`
}
);
}
@ -167,7 +167,7 @@ function createGetPasswordlessAuth(app) {
Looks like the link you clicked has expired,
please request a fresh link, to sign in.
`,
redirectTo: `${homeLocation}/signin`
redirectTo: `${origin}/signin`
});
}
return authToken.destroy$();
@ -184,7 +184,7 @@ function createGetPasswordlessAuth(app) {
'success',
'Success! You have signed in to your account. Happy Coding!'
);
return res.redirectWithFlash(`${homeLocation}/learn`);
return res.redirectWithFlash(`${origin}/learn`);
})
.subscribe(() => {}, next)
);

View File

@ -29,7 +29,6 @@ import {
import { oldDataVizId } from '../../../config/misc';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
import { completeCommitment$ } from '../utils/commit';
import { getChallenges } from '../utils/get-curriculum';
const log = debug('fcc:certification');
@ -354,9 +353,6 @@ function createVerifyCert(certTypeIds, app) {
return Observable.combineLatest(
// update user data
Observable.fromPromise(updatePromise),
// If user has committed to nonprofit,
// this will complete their pledge
completeCommitment$(user),
// sends notification email is user has all 6 certs
// if not it noop
sendCertifiedEmail(user, Email.send$),

View File

@ -17,10 +17,10 @@ import { dasherize } from '../../../utils/slugs';
import { fixCompletedChallengeItem } from '../../common/utils';
import { getChallenges } from '../utils/get-curriculum';
import {
getParamsFromReq,
getRedirectParams,
getRedirectBase,
normalizeParams
} from '../utils/get-return-to';
} from '../utils/redirection';
const log = debug('fcc:boot:challenges');
@ -34,7 +34,7 @@ export default async function bootChallenge(app, done) {
const redirectToCurrentChallenge = createRedirectToCurrentChallenge(
challengeUrlResolver,
normalizeParams,
getParamsFromReq
getRedirectParams
);
api.post(
@ -336,11 +336,11 @@ function backendChallengeCompleted(req, res, next) {
export function createRedirectToCurrentChallenge(
challengeUrlResolver,
normalizeParams,
getParamsFromReq
getRedirectParams
) {
return async function redirectToCurrentChallenge(req, res, next) {
const { user } = req;
const { origin, pathPrefix } = normalizeParams(getParamsFromReq(req));
const { origin, pathPrefix } = getRedirectParams(req, normalizeParams);
const redirectBase = getRedirectBase(origin, pathPrefix);
if (!user) {

View File

@ -1,194 +0,0 @@
import _ from 'lodash';
import { Observable } from 'rx';
import debugFactory from 'debug';
import dedent from 'dedent';
import { homeLocation } from '../../../config/env';
import nonprofits from '../utils/commit.json';
import { commitGoals, completeCommitment$ } from '../utils/commit';
import { unDasherize } from '../../../utils/slugs';
import { observeQuery, saveInstance } from '../utils/rx';
import { ifNoUserRedirectTo } from '../utils/middleware';
const sendNonUserToSignIn = ifNoUserRedirectTo(
`${homeLocation}/signin`,
'You must be signed in to commit to a nonprofit.',
'info'
);
const sendNonUserToCommit = ifNoUserRedirectTo(
'/commit',
'You must be signed in to update commit',
'info'
);
const debug = debugFactory('fcc:commit');
function findNonprofit(name) {
let nonprofit;
if (name) {
nonprofit = _.find(nonprofits, nonprofit => {
return name === nonprofit.name;
});
}
nonprofit = nonprofit || nonprofits[_.random(0, nonprofits.length - 1)];
return nonprofit;
}
export default function commit(app) {
const router = app.loopback.Router();
const api = app.loopback.Router();
const { Pledge } = app.models;
router.get('/commit', commitToNonprofit);
router.get('/commit/pledge', sendNonUserToSignIn, pledge);
router.get('/commit/directory', renderDirectory);
api.post('/commit/stop-commitment', sendNonUserToCommit, stopCommit);
api.post('/commit/complete-goal', sendNonUserToCommit, completeCommitment);
app.use(api);
app.use(router);
function commitToNonprofit(req, res, next) {
const { user } = req;
let nonprofitName = unDasherize(req.query.nonprofit);
debug('looking for nonprofit', nonprofitName);
const nonprofit = findNonprofit(nonprofitName);
Observable.just(user)
.flatMap(user => {
if (user) {
debug('getting user pledge');
return observeQuery(user, 'pledge');
}
return Observable.just();
})
.subscribe(pledge => {
if (pledge) {
debug('found previous pledge');
req.flash(
'info',
dedent`
Looks like you already have a pledge to ${pledge.displayName}.
Clicking "Commit" here will replace your old commitment. If you
do change your commitment, please remember to cancel your
previous recurring donation directly with ${pledge.displayName}.
`
);
}
res.render('commit/', {
title: 'Commit to a nonprofit. Commit to your goal.',
pledge,
...commitGoals,
...nonprofit
});
}, next);
}
function pledge(req, res, next) {
const { user } = req;
const {
nonprofit: nonprofitName = 'girl develop it',
amount = '5',
goal = commitGoals.respWebDesignCert
} = req.query;
const nonprofit = findNonprofit(nonprofitName);
observeQuery(user, 'pledge')
.flatMap(oldPledge => {
// create new pledge for user
const pledge = Pledge({
amount,
goal,
userId: user.id,
...nonprofit
});
if (oldPledge) {
debug('user already has pledge, creating a new one');
// we orphan last pledge since a user only has one pledge at a time
oldPledge.userId = '';
oldPledge.formerUser = user.id;
oldPledge.endDate = new Date();
oldPledge.isOrphaned = true;
return saveInstance(oldPledge).flatMap(() => {
return saveInstance(pledge);
});
}
return saveInstance(pledge);
})
.subscribe(({ displayName, goal, amount }) => {
req.flash(
'success',
dedent`
Congratulations, you have committed to giving
${displayName} $${amount} each month until you have completed
your ${goal}. Please remember to cancel your pledge directly
with ${displayName} once you finish.
`
);
res.redirect('/' + user.username);
}, next);
}
function renderDirectory(req, res) {
res.render('commit/directory', {
title: 'Commit Directory',
nonprofits
});
}
function completeCommitment(req, res, next) {
const { user } = req;
return completeCommitment$(user).subscribe(msgOrPledge => {
if (typeof msgOrPledge === 'string') {
return res.send(msgOrPledge);
}
return res.send(true);
}, next);
}
function stopCommit(req, res, next) {
const { user } = req;
observeQuery(user, 'pledge')
.flatMap(pledge => {
if (!pledge) {
return Observable.just();
}
pledge.formerUserId = pledge.userId;
pledge.userId = null;
pledge.isOrphaned = true;
pledge.dateEnded = new Date();
return saveInstance(pledge);
})
.subscribe(pledge => {
let msg = dedent`
You have successfully stopped your pledge. Please
remember to cancel your recurring donation directly
with the nonprofit if you haven't already done so.
`;
if (!pledge) {
msg = dedent`
It doesn't look like you had an active pledge, so
there's no pledge to stop.
`;
}
req.flash('info', msg);
return res.redirect(`/${user.username}`);
}, next);
}
}

View File

@ -1,8 +1,7 @@
import request from 'request';
import { homeLocation } from '../../../config/env';
import constantStrings from '../utils/constantStrings.json';
import { getRedirectParams } from '../utils/redirection';
const githubClient = process.env.GITHUB_ID;
const githubSecret = process.env.GITHUB_SECRET;
@ -51,19 +50,21 @@ module.exports = function(app) {
'We are no longer able to process this unsubscription request. ' +
'Please go to your settings to update your email preferences'
);
res.redirectWithFlash(homeLocation);
const { origin } = getRedirectParams(req);
res.redirectWithFlash(origin);
}
function unsubscribeById(req, res, next) {
const { origin } = getRedirectParams(req);
const { unsubscribeId } = req.params;
if (!unsubscribeId) {
req.flash('info', 'We could not find an account to unsubscribe');
return res.redirectWithFlash(homeLocation);
return res.redirectWithFlash(origin);
}
return User.find({ where: { unsubscribeId } }, (err, users) => {
if (err || !users.length) {
req.flash('info', 'We could not find an account to unsubscribe');
return res.redirectWithFlash(homeLocation);
return res.redirectWithFlash(origin);
}
const updates = users.map(user => {
return new Promise((resolve, reject) =>
@ -88,7 +89,7 @@ module.exports = function(app) {
"We've successfully updated your email preferences."
);
return res.redirectWithFlash(
`${homeLocation}/unsubscribed/${unsubscribeId}`
`${origin}/unsubscribed/${unsubscribeId}`
);
})
.catch(next);
@ -111,17 +112,18 @@ module.exports = function(app) {
function resubscribe(req, res, next) {
const { unsubscribeId } = req.params;
const { origin } = getRedirectParams(req);
if (!unsubscribeId) {
req.flash(
'info',
'We we unable to process this request, please check and try againÍ'
);
res.redirect(homeLocation);
res.redirect(origin);
}
return User.find({ where: { unsubscribeId } }, (err, users) => {
if (err || !users.length) {
req.flash('info', 'We could not find an account to resubscribe');
return res.redirectWithFlash(homeLocation);
return res.redirectWithFlash(origin);
}
const [user] = users;
return new Promise((resolve, reject) =>
@ -144,7 +146,7 @@ module.exports = function(app) {
"We've successfully updated your email preferences. Thank you " +
'for resubscribing.'
);
return res.redirectWithFlash(homeLocation);
return res.redirectWithFlash(origin);
})
.catch(next);
});

View File

@ -4,19 +4,19 @@ import { pick } from 'lodash';
import { Observable } from 'rx';
import { body } from 'express-validator';
import { homeLocation } from '../../../config/env';
import {
getProgress,
normaliseUserFields,
userPropsForSession
} from '../utils/publicUserProps';
import { fixCompletedChallengeItem } from '../../common/utils';
import { ifNoUser401, ifNoUserRedirectTo } from '../utils/middleware';
import { ifNoUser401, ifNoUserRedirectHome } from '../utils/middleware';
import { removeCookies } from '../utils/getSetAccessToken';
import { trimTags } from '../utils/validators';
import { getRedirectParams } from '../utils/redirection';
const log = debugFactory('fcc:boot:user');
const sendNonUserToHome = ifNoUserRedirectTo(homeLocation);
const sendNonUserToHome = ifNoUserRedirectHome();
function bootUser(app) {
const api = app.loopback.Router();
@ -100,7 +100,7 @@ function getAccount(req, res) {
function getUnlinkSocial(req, res, next) {
const { user } = req;
const { username } = user;
const { origin } = getRedirectParams(req);
let social = req.params.social;
if (!social) {
req.flash('danger', 'No social account found');
@ -151,7 +151,7 @@ function getUnlinkSocial(req, res, next) {
log(`${social} has been unlinked successfully`);
req.flash('info', `You've successfully unlinked your ${social}.`);
return res.redirectWithFlash(`${homeLocation}/${username}`);
return res.redirectWithFlash(`${origin}/${username}`);
});
});
});
@ -209,7 +209,7 @@ function createPostReportUserProfile(app) {
return function postReportUserProfile(req, res, next) {
const { user } = req;
const { username, reportDescription: report } = req.body;
const { origin } = getRedirectParams(req);
log(username);
log(report);
@ -241,7 +241,7 @@ function createPostReportUserProfile(app) {
},
err => {
if (err) {
err.redirectTo = `${homeLocation}/${username}`;
err.redirectTo = `${origin}/${username}`;
return next(err);
}

View File

@ -1,16 +1,16 @@
import accepts from 'accepts';
import { homeLocation } from '../../../config/env';
import { getRedirectParams } from '../utils/redirection';
export default function fourOhFour(app) {
app.all('*', function(req, res) {
const accept = accepts(req);
const type = accept.type('html', 'json', 'text');
const { path } = req;
const { origin } = getRedirectParams(req);
if (type === 'html') {
req.flash('danger', `We couldn't find path ${path}`);
return res.redirectWithFlash(`${homeLocation}/404`);
return res.redirectWithFlash(`${origin}/404`);
}
if (type === 'json') {

View File

@ -13,9 +13,9 @@ import { jwtSecret } from '../../config/secrets';
import {
getReturnTo,
getRedirectBase,
getParamsFromReq,
getRedirectParams,
isRootPath
} from './utils/get-return-to';
} from './utils/redirection';
const passportOptions = {
emailOptional: true,
@ -86,7 +86,7 @@ export const devSaveResponseAuthCookies = () => {
export const devLoginRedirect = () => {
return (req, res) => {
// this mirrors the production approach, but without any validation
let { returnTo, origin, pathPrefix } = getParamsFromReq(req);
let { returnTo, origin, pathPrefix } = getRedirectParams(req);
returnTo += isRootPath(getRedirectBase(origin, pathPrefix), returnTo)
? '/learn'
: '';

View File

@ -2,9 +2,8 @@
// import _ from 'lodash/fp';
import accepts from 'accepts';
import { homeLocation } from '../../../config/env';
import { unwrapHandledError } from '../utils/create-handled-error.js';
import { getRedirectParams } from '../utils/redirection';
const errTemplate = (error, req) => {
const { message, stack } = error;
@ -27,6 +26,7 @@ export default function prodErrorHandler() {
// error handling in production.
// eslint-disable-next-line no-unused-vars
return function(err, req, res, next) {
const { origin } = getRedirectParams(req);
const handled = unwrapHandledError(err);
// respect handled error status
let status = handled.status || err.status || res.statusCode;
@ -39,7 +39,7 @@ export default function prodErrorHandler() {
const accept = accepts(req);
const type = accept.type('html', 'json', 'text');
const redirectTo = handled.redirectTo || `${homeLocation}/`;
const redirectTo = handled.redirectTo || `${origin}/`;
const message =
handled.message ||
'Oops! Something went wrong. Please try again in a moment.';

View File

@ -6,10 +6,10 @@ import {
errorTypes,
authHeaderNS
} from '../utils/getSetAccessToken';
import { homeLocation } from '../../../config/env';
import { jwtSecret as _jwtSecret } from '../../../config/secrets';
import { wrapHandledError } from '../utils/create-handled-error';
import { getRedirectParams } from '../utils/redirection';
const authRE = /^\/auth\//;
const confirmEmailRE = /^\/confirm-email$/;
@ -50,6 +50,7 @@ export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) {
export default ({ jwtSecret = _jwtSecret, getUserById = _getUserById } = {}) =>
function requestAuthorisation(req, res, next) {
const { origin } = getRedirectParams(req);
const { path } = req;
if (!isAllowedPath(path)) {
const { accessToken, error, jwt } = getAccessTokenFromRequest(
@ -61,7 +62,7 @@ export default ({ jwtSecret = _jwtSecret, getUserById = _getUserById } = {}) =>
new Error('Access token is required for this request'),
{
type: 'info',
redirect: `${homeLocation}/signin`,
redirect: `${origin}/signin`,
message: 'Access token is required for this request',
status: 403
}
@ -70,7 +71,7 @@ export default ({ jwtSecret = _jwtSecret, getUserById = _getUserById } = {}) =>
if (!accessToken && error === errorTypes.invalidToken) {
throw wrapHandledError(new Error('Access token is invalid'), {
type: 'info',
redirect: `${homeLocation}/signin`,
redirect: `${origin}/signin`,
message: 'Your access token is invalid',
status: 403
});
@ -78,7 +79,7 @@ export default ({ jwtSecret = _jwtSecret, getUserById = _getUserById } = {}) =>
if (!accessToken && error === errorTypes.expiredToken) {
throw wrapHandledError(new Error('Access token is no longer valid'), {
type: 'info',
redirect: `${homeLocation}/signin`,
redirect: `${origin}/signin`,
message: 'Access token is no longer valid',
status: 403
});

View File

@ -1,8 +1,9 @@
/* global describe it expect */
import sinon from 'sinon';
import { mockReq, mockRes } from 'sinon-express-mock';
import { mockReq as mockRequest, mockRes } from 'sinon-express-mock';
import jwt from 'jsonwebtoken';
import { homeLocation } from '../../../config/env.json';
import createRequestAuthorization, {
isAllowedPath
} from './request-authorization';
@ -26,6 +27,12 @@ const users = {
const mockGetUserById = id =>
id in users ? Promise.resolve(users[id]) : Promise.reject('No user found');
const mockReq = args => {
const mock = mockRequest(args);
mock.header = () => homeLocation;
return mock;
};
describe('request-authorization', () => {
describe('isAllowedPath', () => {
const authRE = /^\/auth\//;

View File

@ -3,6 +3,7 @@ import { homeLocation, apiLocation } from '../../config/env';
const { clientID, clientSecret, domain } = auth0;
// These don't seem to be used, can they go?
const successRedirect = `${homeLocation}/learn`;
const failureRedirect = `${homeLocation}/signin`;

View File

@ -1,16 +0,0 @@
{
"frontEndCert": "Front End Development Certification",
"backEndCert": "Back End Development Certification",
"fullStackCert": "Full Stack Development Certification",
"respWebDesignCert": "Responsive Web Design Certification",
"frontEndLibsCert": "Front End Libraries Certification",
"jsAlgoDataStructCert": "JavaScript Algorithms and Data Structures Certification",
"dataVisCert": "Data Visualisation Certification",
"apisMicroservicesCert": "APIs and Microservices Certification",
"infosecQaCert": "Information Security and Quality Assurance Certification",
"qaCert": "Quality Assurance Certification",
"infosecCert": "Information Security Certification",
"sciCompPyCert": "Scientific Computing with Python Certification",
"dataAnalysisPyCert": "Data Analysis with Python Certification",
"machineLearningPyCert": "Machine Learning with Python Certification"
}

View File

@ -1,63 +0,0 @@
import dedent from 'dedent';
import debugFactory from 'debug';
import { Observable } from 'rx';
import commitGoals from './commit-goals.json';
const debug = debugFactory('fcc:utils/commit');
export { commitGoals };
export function completeCommitment$(user) {
const {
isFrontEndCert,
isBackEndCert,
isFullStackCert,
isRespWebDesignCert,
isFrontEndLibsCert,
isJsAlgoDataStructCert,
isDataVisCert,
isApisMicroservicesCert,
isInfosecQaCert,
isQaCertV7,
isInfosecCertV7,
isSciCompPyCertV7,
isDataAnalysisPyCertV7,
isMachineLearningPyCertV7
} = user;
return Observable.fromNodeCallback(user.pledge, user)().flatMap(pledge => {
if (!pledge) {
return Observable.just('No pledge found');
}
const { goal } = pledge;
if (
(isFrontEndCert && goal === commitGoals.frontEndCert) ||
(isBackEndCert && goal === commitGoals.backEndCert) ||
(isFullStackCert && goal === commitGoals.fullStackCert) ||
(isRespWebDesignCert && goal === commitGoals.respWebDesignCert) ||
(isFrontEndLibsCert && goal === commitGoals.frontEndLibsCert) ||
(isJsAlgoDataStructCert && goal === commitGoals.jsAlgoDataStructCert) ||
(isDataVisCert && goal === commitGoals.dataVisCert) ||
(isApisMicroservicesCert && goal === commitGoals.apisMicroservicesCert) ||
(isInfosecQaCert && goal === commitGoals.infosecQaCert) ||
(isQaCertV7 && goal === commitGoals.QaCert) ||
(isInfosecCertV7 && goal === commitGoals.infosecCert) ||
(isSciCompPyCertV7 && goal === commitGoals.sciCompPyCert) ||
(isDataAnalysisPyCertV7 && goal === commitGoals.dataAnalysisPyCert) ||
(isMachineLearningPyCertV7 && goal === commitGoals.machineLearningPyCert)
) {
debug('marking goal complete');
pledge.isCompleted = true;
pledge.dateEnded = new Date();
pledge.formerUserId = pledge.userId;
pledge.userId = null;
return Observable.fromNodeCallback(pledge.save, pledge)();
}
return Observable.just(dedent`
You have not yet reached your goal of completing the ${goal}
Please retry when you have met the requirements.
`);
});
}

View File

@ -1,50 +0,0 @@
[
{
"name": "girl develop it",
"displayName": "Girl Develop It",
"donateUrl": "https://www.girldevelopit.com/donate",
"description": "Girl Develop It provides in-person classes for women to learn to code.",
"imgAlt": "Girl Develop It participants coding at tables.",
"imgUrl": "https://cdn-media-1.freecodecamp.org/imgr/U1CyEuA.jpg"
},
{
"name": "black girls code",
"displayName": "Black Girls CODE",
"donateUrl": "http://www.blackgirlscode.com/",
"description": "Black Girls CODE is devoted to showing the world that black girls can code, and do so much more.",
"imgAlt": "Girls developing code with instructor",
"imgUrl": "https://cdn-media-1.freecodecamp.org/imgr/HBVrdaj.jpg"
},
{
"name": "coderdojo",
"displayName": "CoderDojo",
"donateUrl": "https://www.globalgiving.org/projects/coderdojo-start-a-dojo-support/",
"description": "CoderDojo is the global network of free computer programming clubs for young people.",
"imgAlt": "Two adults help several kids program on their laptops.",
"imgUrl": "https://cdn-media-1.freecodecamp.org/imgr/701RLfV.jpg"
},
{
"name": "women who code",
"displayName": "Women Who Code",
"donateUrl": "https://www.womenwhocode.com/donate",
"description": "Women Who Code (WWCode) is a global leader in propelling women in the tech industry.",
"imgAlt": "Four women sitting in a classroom together learning to code.",
"imgUrl": "https://cdn-media-1.freecodecamp.org/imgr/tKUi6Rf.jpg"
},
{
"name": "girls who code",
"displayName": "Girls Who Code",
"donateUrl": "http://girlswhocode.com/",
"description": "Girls Who Code programs work to inspire, educate, and equip girls with the computing skills to pursue 21st century opportunities.",
"imgAlt": "Three women smiling while they code on a computer together.",
"imgUrl": "https://cdn-media-1.freecodecamp.org/imgr/op8BVph.jpg"
},
{
"name": "hack club",
"displayName": "Hack Club",
"donateUrl": "https://hackclub.com/donate",
"description": "Hack Club helps high schoolers start after-school coding clubs.",
"imgAlt": "A bunch of high school students posing for a photo in their Hack Club.",
"imgUrl": "https://cdn-media-1.freecodecamp.org/imgr/G2YvPHf.jpg"
}
]

View File

@ -1,60 +0,0 @@
/* global describe expect it */
const { homeLocation } = require('../../../config/env.json');
const jwt = require('jsonwebtoken');
const { getReturnTo } = require('./get-return-to');
const validJWTSecret = 'this is a super secret string';
const invalidJWTSecret = 'This is not correct secret';
const validReturnTo = 'https://www.freecodecamp.org/settings';
const invalidReturnTo = 'https://www.freecodecamp.org.fake/settings';
const defaultReturnTo = `${homeLocation}/learn`;
const defaultOrigin = homeLocation;
const defaultPrefix = '';
const defaultObject = {
returnTo: defaultReturnTo,
origin: defaultOrigin,
pathPrefix: defaultPrefix
};
describe('get-return-to', () => {
describe('getReturnTo', () => {
it('should extract returnTo from a jwt', () => {
expect.assertions(1);
const encryptedReturnTo = jwt.sign(
{ returnTo: validReturnTo },
validJWTSecret
);
expect(getReturnTo(encryptedReturnTo, validJWTSecret)).toStrictEqual({
...defaultObject,
returnTo: validReturnTo
});
});
it('should return a default url if the secrets do not match', () => {
expect.assertions(1);
const encryptedReturnTo = jwt.sign(
{ returnTo: validReturnTo },
invalidJWTSecret
);
expect(getReturnTo(encryptedReturnTo, validJWTSecret)).toStrictEqual(
defaultObject
);
});
it('should return a default url for unknown origins', () => {
expect.assertions(1);
const encryptedReturnTo = jwt.sign(
{ returnTo: invalidReturnTo },
validJWTSecret
);
expect(getReturnTo(encryptedReturnTo, validJWTSecret)).toStrictEqual(
defaultObject
);
});
});
});

View File

@ -2,22 +2,23 @@ import dedent from 'dedent';
import { validationResult } from 'express-validator';
import { createValidatorErrorFormatter } from './create-handled-error.js';
import { homeLocation } from '../../../config/env';
import {
getAccessTokenFromRequest,
removeCookies
} from './getSetAccessToken.js';
import { getRedirectParams } from './redirection';
export function ifNoUserRedirectTo(url, message, type = 'errors') {
export function ifNoUserRedirectHome(message, type = 'errors') {
return function(req, res, next) {
const { path } = req;
if (req.user) {
return next();
}
const { origin } = getRedirectParams(req);
req.flash(type, message || `You must be signed in to access ${path}`);
return res.redirect(url);
return res.redirect(origin);
};
}
@ -55,15 +56,13 @@ export function ifNotVerifiedRedirectToUpdateEmail(req, res, next) {
return next();
}
export function ifUserRedirectTo(path = `${homeLocation}/learn`, status) {
export function ifUserRedirectTo(status) {
status = status === 301 ? 301 : 302;
return (req, res, next) => {
const { accessToken } = getAccessTokenFromRequest(req);
const { returnTo } = getRedirectParams(req);
if (req.user && accessToken) {
if (req.query && req.query.returnTo) {
return res.status(status).redirect(req.query.returnTo);
}
return res.status(status).redirect(path);
return res.status(status).redirect(returnTo);
}
if (req.user && !accessToken) {
// This request has an active auth session

View File

@ -1,30 +1,33 @@
const jwt = require('jsonwebtoken');
const { availableLangs } = require('../../../client/i18n/allLangs');
const { allowedOrigins } = require('../../../config/cors-settings');
// homeLocation is being used as a fallback, here. If the one provided by the
// homeLocation is being used as a fallback here. If the one provided by the
// client is invalid we default to this.
const { homeLocation } = require('../../../config/env.json');
function getReturnTo(encryptedReturnTo, secret) {
function getReturnTo(encryptedParams, secret, _homeLocation = homeLocation) {
let params;
try {
params = jwt.verify(encryptedReturnTo, secret);
params = jwt.verify(encryptedParams, secret);
} catch (e) {
// TODO: report to Sentry? Probably not. Remove entirely?
console.log(e);
// something went wrong, use default params
params = {
returnTo: `${homeLocation}/learn`,
origin: homeLocation,
returnTo: `${_homeLocation}/learn`,
origin: _homeLocation,
pathPrefix: ''
};
}
return normalizeParams(params);
return normalizeParams(params, _homeLocation);
}
// TODO: tests!
function normalizeParams({ returnTo, origin, pathPrefix }) {
function normalizeParams(
{ returnTo, origin, pathPrefix },
_homeLocation = homeLocation
) {
// coerce to strings, just in case something weird and nefarious is happening
returnTo = '' + returnTo;
origin = '' + origin;
@ -35,20 +38,20 @@ function normalizeParams({ returnTo, origin, pathPrefix }) {
!returnTo ||
!allowedOrigins.some(allowed => returnTo.startsWith(allowed + '/'))
) {
returnTo = `${homeLocation}/learn`;
returnTo = `${_homeLocation}/learn`;
origin = _homeLocation;
pathPrefix = '';
}
// this can be strict equality.
if (!origin || !allowedOrigins.includes(origin)) {
origin = homeLocation;
returnTo = `${_homeLocation}/learn`;
origin = _homeLocation;
pathPrefix = '';
}
// default to '' if the locale isn't recognised
pathPrefix = availableLangs.client.includes(pathPrefix) ? pathPrefix : '';
return { returnTo, origin, pathPrefix };
}
// TODO: use this to redirect to current challenge
// TODO: tests!
// TODO: ensure origin and pathPrefix validation happens first
// (it needs a dedicated function that can be called from here and getReturnTo)
function getRedirectBase(origin, pathPrefix) {
@ -59,7 +62,7 @@ function getRedirectBase(origin, pathPrefix) {
// TODO: this might be cleaner if we just use a URL for returnTo (call it
// returnURL for clarity) rather than pulling out origin and returning it
// separately
function getParamsFromReq(req) {
function getRedirectParams(req, _normalizeParams = normalizeParams) {
const url = req.header('Referer');
// since we do not always redirect the user back to the page they were on
// we need client locale and origin to construct the redirect url.
@ -68,7 +71,7 @@ function getParamsFromReq(req) {
// if this is not one of the client languages, validation will convert
// this to '' before it is used.
const pathPrefix = returnUrl.pathname.split('/')[0];
return { returnTo: returnUrl.href, origin, pathPrefix };
return _normalizeParams({ returnTo: returnUrl.href, origin, pathPrefix });
}
function isRootPath(redirectBase, returnUrl) {
@ -80,5 +83,5 @@ function isRootPath(redirectBase, returnUrl) {
module.exports.getReturnTo = getReturnTo;
module.exports.getRedirectBase = getRedirectBase;
module.exports.normalizeParams = normalizeParams;
module.exports.getParamsFromReq = getParamsFromReq;
module.exports.getRedirectParams = getRedirectParams;
module.exports.isRootPath = isRootPath;

View File

@ -0,0 +1,120 @@
/* global describe expect it */
const jwt = require('jsonwebtoken');
const { getReturnTo, normalizeParams } = require('./redirection');
const validJWTSecret = 'this is a super secret string';
const invalidJWTSecret = 'This is not correct secret';
const validReturnTo = 'https://www.freecodecamp.org/settings';
const invalidReturnTo = 'https://www.freecodecamp.org.fake/settings';
const defaultReturnTo = 'https://www.freecodecamp.org/learn';
const defaultOrigin = 'https://www.freecodecamp.org';
const defaultPrefix = '';
const defaultObject = {
returnTo: defaultReturnTo,
origin: defaultOrigin,
pathPrefix: defaultPrefix
};
describe('redirection', () => {
describe('getReturnTo', () => {
it('should extract returnTo from a jwt', () => {
expect.assertions(1);
const encryptedReturnTo = jwt.sign(
{ returnTo: validReturnTo, origin: defaultOrigin },
validJWTSecret
);
expect(
getReturnTo(encryptedReturnTo, validJWTSecret, defaultOrigin)
).toStrictEqual({
...defaultObject,
returnTo: validReturnTo
});
});
it('should return a default url if the secrets do not match', () => {
expect.assertions(1);
const encryptedReturnTo = jwt.sign(
{ returnTo: validReturnTo },
invalidJWTSecret
);
expect(
getReturnTo(encryptedReturnTo, validJWTSecret, defaultOrigin)
).toStrictEqual(defaultObject);
});
it('should return a default url for unknown origins', () => {
expect.assertions(1);
const encryptedReturnTo = jwt.sign(
{ returnTo: invalidReturnTo },
validJWTSecret
);
expect(
getReturnTo(encryptedReturnTo, validJWTSecret, defaultOrigin)
).toStrictEqual(defaultObject);
});
});
describe('normalizeParams', () => {
it('should return a {returnTo, origin, pathPrefix} object', () => {
expect.assertions(2);
const keys = Object.keys(normalizeParams({}));
const expectedKeys = ['returnTo', 'origin', 'pathPrefix'];
expect(keys.length).toBe(3);
expect(keys).toEqual(expect.arrayContaining(expectedKeys));
});
it('should default to homeLocation', () => {
expect.assertions(1);
expect(normalizeParams({}, defaultOrigin)).toEqual(defaultObject);
});
it('should convert an unknown pathPrefix to ""', () => {
expect.assertions(1);
const brokenPrefix = {
...defaultObject,
pathPrefix: 'not-really-a-name'
};
expect(normalizeParams(brokenPrefix, defaultOrigin)).toEqual(
defaultObject
);
});
it('should not change a known pathPrefix', () => {
expect.assertions(1);
const spanishPrefix = {
...defaultObject,
pathPrefix: 'espanol'
};
expect(normalizeParams(spanishPrefix, defaultOrigin)).toEqual(
spanishPrefix
);
});
// we *could*, in principle, grab the path and send them to
// homeLocation/path, but if the origin is wrong something unexpected is
// going on. In that case it's probably best to just send them to
// homeLocation/learn.
it('should return default parameters if the origin is unknown', () => {
expect.assertions(1);
const exampleOrigin = {
...defaultObject,
origin: 'http://example.com',
pathPrefix: 'espanol'
};
expect(normalizeParams(exampleOrigin, defaultOrigin)).toEqual(
defaultObject
);
});
it('should return default parameters if the returnTo is unknown', () => {
expect.assertions(1);
const exampleReturnTo = {
...defaultObject,
returnTo: 'http://example.com/path',
pathPrefix: 'espanol'
};
expect(normalizeParams(exampleReturnTo, defaultOrigin)).toEqual(
defaultObject
);
});
});
});

View File

@ -1,14 +0,0 @@
extends ../layout
block content
h1.text-center Commit to one of these nonprofits
hr
.row
.col-xs-12.col-sm-10.col-sm-offset-1
for nonprofit in nonprofits
.col-xs-12.col-sm-6.col-md-4.height-400
.text-center
h2= nonprofit.displayName
img.testimonial-image.img-responsive.img-center(src=nonprofit.imgUrl)
.button-spacer
a.text-center(href='/commit?nonprofit=#{nonprofit.name}') Commit to #{nonprofit.displayName}
p= nonprofit.description

View File

@ -1,110 +0,0 @@
extends ../layout
block content
h2.text-center Commit to yourself. Commit to a nonprofit.
.row
.col-xs-12.col-sm-6.col-sm-offset-3
p You can give yourself external motivation, and immediately start helping nonprofits. You can do this by pledging a monthly donation to a nonprofit until youve earned one of our certifications. This pledge is completely optional. This pledge is entirely between you and the nonprofit, and no money goes to freeCodeCamp. You can change your commitment or stop it at any time.
.row
.col-xs-12.col-sm-6.col-sm-offset-3.text-center
h3 Pledge to #{displayName} 
.button-spacer
a(href='#{imgUrl}' data-lightbox='img-enlarge' alt='#{imgAlt}')
img.img-responsive(src='#{imgUrl}' alt='#{imgAlt}')
p.large-p
= description
p
a(href='/commit/directory') ...or see other nonprofits
.spacer
form.form(name='commit')
.hidden
input(type='text' value='#{name}' name='nonprofit')
.row
.col-xs-12.col-sm-6.col-sm-offset-3
h3 Step 1: Which certification do you pledge to complete?
.btn-group-vertical(data-toggle='buttons' role='group')
label.btn.btn-primary.active
input(type='radio' id="respWebDesignCert" value="Responsive Web Design Certification" name='goal' checked="checked")
| Responsive Web Design
label.btn.btn-primary
input(type='radio' id="frontEndLibsCert" value="Front End Libraries Certification" name='goal')
| Front End Libraries
label.btn.btn-primary
input(type='radio' id="jsAlgoDataStructCert" value="JavaScript Algorithms and Data Structures Certification" name='goal')
| JavaScript Algorithms and Data Structures
label.btn.btn-primary
input(type='radio' id="dataVisCert" value="Data Visualization Certification" name='goal')
| Data Visualization
label.btn.btn-primary
input(type='radio' id="apisMicroservicesCert" value="APIs and Microservices Certification" name='goal')
| APIs and Microservices
label.btn.btn-primary
input(type='radio' id="infosecQaCert" value="Information Security and Quality Assurance Certification" name='goal')
| Information Security and Quality Assurance
.spacer
.row
.col-xs-12.col-sm-6.col-sm-offset-3
h3 Step 2: How much do you want to pledge monthly until you earn that certification?
.btn-group.btn-group-justified(data-toggle='buttons' role='group')
label.btn.btn-primary
input(type='radio' id='5-dollar-pledge' value='5' name='amount')
| $5 per month
label.btn.btn-primary.active
input(type='radio' id='10-dollar-pledge' value='10' name='amount' checked="checked")
| $10 per month
label.btn.btn-primary
input(type='radio' id='25-dollar-pledge' value='25' name='amount')
| $25 per month
label.btn.btn-primary
input(type='radio' id='50-dollar-pledge' value='50' name='amount')
| $50 per month
.spacer
.col-xs-12.col-sm-6.col-sm-offset-3
h3 Step 3: Set up your monthly donation
.row
.col-xs-12.col-sm-6.col-sm-offset-3.text-center
a#commit-btn-donate.btn.btn-block.btn-lg.btn-primary(href=donateUrl target='_blank') Open the #{displayName} donation page
.spacer
.col-xs-12.col-sm-6.col-sm-offset-3
h3#commit-step4-text.disabled
Step 4: Confirm
span#commit-step4-hidden.disabled (Do step 3 first)
span#commit-step4-show.hidden your commitment to your goal
.row
.col-xs-12.col-sm-6.col-sm-offset-3.text-center
button#commit-btn-submit.btn.btn-block.btn-lg.btn-primary.disabled Commit
if pledge
form.row(name='stop-pledge' action='/commit/stop-commitment' method='post')
input(type='hidden', name='_csrf', value=_csrf)
.col-xs-12.col-sm-6.col-sm-offset-3.text-center
.button-spacer
button.btn.btn-block.btn-lg.btn-default(name='submit' type='submit') Stop my current pledge
else
.row
.col-xs-12.col-sm-6.col-sm-offset-3.text-center
.button-spacer
a.btn.btn-block.btn-lg.btn-default(href='/map') Maybe later
script.
$(function() {
$('#commit-btn-donate').click(function() {
$('#commit-btn-submit').removeClass('disabled');
$('#commit-step4-text').removeClass('disabled');
$('#commit-step4-hidden').hide();
$('#commit-step4-show').removeClass('hidden');
});
$('#commit-btn-submit').click(function() {
if (
history &&
typeof history.pushState === 'function'
) {
history.pushState(history.state, null, '/commit/pledge?' + $('form').serialize());
return null;
}
window.location.href = '/commit/pledge?' + $('form').serialize();
});
});

View File

@ -1,15 +0,0 @@
extends ../layout
block content
.panel.panel-info
.panel-body
h3.text-center You've committed!
.row
.col-xs-12.col-sm-6.col-sm-offset-3
p Congratulations, you have committed to giving
span(style='text-transform: capitalize') #{nonprofit}
| #{amount} dollars a month until you have reached your goal
| of completing your #{goal}
.row
.col-xs-12.col-sm-6.col-sm-offset-3
img.img-responsive(src='https://cdn-media-1.freecodecamp.org/imgr/U1CyEuA.jpg' alt="Girl Develop It participants coding at tables.")
p Girl Develop It is a nonprofit that provides in-person classes for women to learn to code.