fix(api): only use homeLocation as a fallback (#40517)
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
03fa21a565
commit
a076547d43
@ -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)
|
||||
);
|
||||
|
@ -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$),
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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') {
|
||||
|
@ -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'
|
||||
: '';
|
||||
|
@ -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.';
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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\//;
|
||||
|
@ -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`;
|
||||
|
||||
|
@ -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"
|
||||
}
|
@ -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.
|
||||
`);
|
||||
});
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
|
@ -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;
|
120
api-server/server/utils/redirection.test.js
Normal file
120
api-server/server/utils/redirection.test.js
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
@ -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 you’ve 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();
|
||||
});
|
||||
});
|
@ -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.
|
Reference in New Issue
Block a user