chore(server): Move api-server in to it's own DIR

This commit is contained in:
Bouncey
2018-08-31 16:04:04 +01:00
committed by mrugesh mohapatra
parent 9fba6bce4c
commit 46a217d0a5
369 changed files with 328 additions and 7431 deletions

View File

@ -0,0 +1,15 @@
import { Observable } from 'rx';
export default function extendEmail(app) {
const { AccessToken, Email } = app.models;
Email.send$ = Observable.fromNodeCallback(Email.send, Email);
AccessToken.findOne$ = Observable.fromNodeCallback(
AccessToken.findOne.bind(AccessToken)
);
AccessToken.prototype.validate$ = Observable.fromNodeCallback(
AccessToken.prototype.validate
);
AccessToken.prototype.destroy$ = Observable.fromNodeCallback(
AccessToken.prototype.destroy
);
}

View File

@ -0,0 +1,5 @@
module.exports = function increaseListers(app) {
// increase loopback database ODM max listeners
// this is a EventEmitter method
app.dataSources.db.setMaxListeners(32);
};

View File

@ -0,0 +1,20 @@
import Fetchr from 'fetchr';
import getUserServices from '../services/user';
import getMapUiServices from '../services/mapUi';
import getChallengesForBlockService from '../services/challenge';
export default function bootServices(app) {
const challenge = getChallengesForBlockService(app);
const mapUi = getMapUiServices(app);
const user = getUserServices(app);
Fetchr.registerFetcher(challenge);
Fetchr.registerFetcher(mapUi);
Fetchr.registerFetcher(user);
const middleware = Fetchr.middleware();
app.use('/services', middleware);
app.use('/external/services', middleware);
app.use('/internal/services', middleware);
}

View File

@ -0,0 +1,220 @@
import _ from 'lodash';
import { Observable } from 'rx';
import dedent from 'dedent';
// import debugFactory from 'debug';
import { isEmail } from 'validator';
import { check } from 'express-validator/check';
import { homeLocation } from '../../../config/env';
import {
ifUserRedirectTo,
ifNoUserRedirectTo,
createValidatorErrorHandler
} from '../utils/middleware';
import { wrapHandledError } from '../utils/create-handled-error.js';
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
// const debug = debugFactory('fcc:boot:auth');
if (isSignUpDisabled) {
console.log('fcc:boot:auth - Sign up is disabled');
}
module.exports = function enableAuthentication(app) {
// enable loopback access control authentication. see:
// loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html
app.enableAuth();
const ifUserRedirect = ifUserRedirectTo();
const ifNoUserRedirectHome = ifNoUserRedirectTo(homeLocation);
const api = app.loopback.Router();
const { AuthToken, User } = app.models;
api.get('/signin', ifUserRedirect, (req, res) => res.redirect('/auth/auth0'));
api.get('/signout', (req, res) => {
req.logout();
req.session.destroy(err => {
if (err) {
throw wrapHandledError(new Error('could not destroy session'), {
type: 'info',
message: 'Oops, something is not right.',
redirectTo: homeLocation
});
}
const config = {
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);
res.redirect(homeLocation);
});
});
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('/internal', api);
};

View File

@ -0,0 +1,450 @@
import _ from 'lodash';
import loopback from 'loopback';
import moment from 'moment-timezone';
import path from 'path';
import dedent from 'dedent';
import { Observable } from 'rx';
import debug from 'debug';
import { isEmail } from 'validator';
import {
ifNoUser401
} from '../utils/middleware';
import { observeQuery } from '../utils/rx';
import {
legacyFrontEndChallengeId,
legacyBackEndChallengeId,
legacyDataVisId,
respWebDesignId,
frontEndLibsId,
jsAlgoDataStructId,
dataVis2018Id,
apisMicroservicesId,
infosecQaId,
fullStackId
} from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
import {
completeCommitment$
} from '../utils/commit';
const log = debug('fcc:certification');
const renderCertifedEmail = loopback.template(path.join(
__dirname,
'..',
'views',
'emails',
'certified.ejs'
));
function isCertified(ids, completedChallenges = []) {
return _.every(
ids,
({ id }) => _.find(
completedChallenges,
({ id: completedId }) => completedId === id
)
);
}
const certIds = {
[certTypes.frontEnd]: legacyFrontEndChallengeId,
[certTypes.backEnd]: legacyBackEndChallengeId,
[certTypes.dataVis]: legacyDataVisId,
[certTypes.respWebDesign]: respWebDesignId,
[certTypes.frontEndLibs]: frontEndLibsId,
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
[certTypes.dataVis2018]: dataVis2018Id,
[certTypes.apisMicroservices]: apisMicroservicesId,
[certTypes.infosecQa]: infosecQaId,
[certTypes.fullStack]: fullStackId
};
const certViews = {
[certTypes.frontEnd]: 'certificate/legacy/front-end.jade',
[certTypes.backEnd]: 'certificate/legacy/back-end.jade',
[certTypes.dataVis]: 'certificate/legacy/data-visualization.jade',
[certTypes.respWebDesign]: 'certificate/responsive-web-design.jade',
[certTypes.frontEndLibs]: 'certificate/front-end-libraries.jade',
[certTypes.jsAlgoDataStruct]:
'certificate/javascript-algorithms-and-data-structures.jade',
[certTypes.dataVis2018]: 'certificate/data-visualization.jade',
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
[certTypes.infosecQa]:
'certificate/information-security-and-quality-assurance.jade',
[certTypes.fullStack]: 'certificate/full-stack.jade'
};
const certText = {
[certTypes.frontEnd]: 'Legacy Front End',
[certTypes.backEnd]: 'Legacy Back End',
[certTypes.dataVis]: 'Legacy Data Visualization',
[certTypes.fullStack]: 'Legacy Full Stack',
[certTypes.respWebDesign]: 'Responsive Web Design',
[certTypes.frontEndLibs]: 'Front End Libraries',
[certTypes.jsAlgoDataStruct]:
'JavaScript Algorithms and Data Structures',
[certTypes.dataVis2018]: 'Data Visualization',
[certTypes.apisMicroservices]: 'APIs and Microservices',
[certTypes.infosecQa]: 'Information Security and Quality Assurance'
};
function getIdsForCert$(id, Challenge) {
return observeQuery(
Challenge,
'findById',
id,
{
id: true,
tests: true,
name: true,
challengeType: true
}
)
.shareReplay();
}
function sendCertifiedEmail(
{
email = '',
name,
username,
isRespWebDesignCert,
isFrontEndLibsCert,
isJsAlgoDataStructCert,
isDataVisCert,
isApisMicroservicesCert,
isInfosecQaCert
},
send$
) {
if (
!isEmail(email) ||
!isRespWebDesignCert ||
!isFrontEndLibsCert ||
!isJsAlgoDataStructCert ||
!isDataVisCert ||
!isApisMicroservicesCert ||
!isInfosecQaCert
) {
return Observable.just(false);
}
const notifyUser = {
type: 'email',
to: email,
from: 'team@freeCodeCamp.org',
subject: dedent`
Congratulations on completing all of the
freeCodeCamp certifications!
`,
text: renderCertifedEmail({
username,
name
})
};
return send$(notifyUser).map(() => true);
}
export default function certificate(app) {
const router = app.loopback.Router();
const { Email, Challenge, User } = app.models;
function findUserByUsername$(username, fields) {
return observeQuery(
User,
'findOne',
{
where: { username },
fields
}
);
}
const certTypeIds = {
// legacy
[certTypes.frontEnd]: getIdsForCert$(legacyFrontEndChallengeId, Challenge),
[certTypes.backEnd]: getIdsForCert$(legacyBackEndChallengeId, Challenge),
[certTypes.dataVis]: getIdsForCert$(legacyDataVisId, Challenge),
// modern
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
[certTypes.frontEndLibs]: getIdsForCert$(frontEndLibsId, Challenge),
[certTypes.dataVis2018]: getIdsForCert$(dataVis2018Id, Challenge),
[certTypes.jsAlgoDataStruct]: getIdsForCert$(jsAlgoDataStructId, Challenge),
[certTypes.apisMicroservices]: getIdsForCert$(
apisMicroservicesId,
Challenge
),
[certTypes.infosecQa]: getIdsForCert$(infosecQaId, Challenge),
[certTypes.fullStack]: getIdsForCert$(fullStackId, Challenge)
};
const superBlocks = Object.keys(superBlockCertTypeMap);
router.get(
'/:username/front-end-certification',
(req, res) => res.redirect(
`/certification/${req.params.username}/legacy-front-end`
)
);
router.get(
'/:username/data-visualization-certification',
(req, res) => res.redirect(
`/certification/${req.params.username}/legacy-data-visualization`
)
);
router.get(
'/:username/back-end-certification',
(req, res) => res.redirect(
`/certification/${req.params.username}/legacy-back-end`
)
);
router.get(
'/:username/full-stack-certification',
(req, res) => res.redirect(
`/certification/${req.params.username}/full-stack`
)
);
router.post(
'/certificate/verify',
ifNoUser401,
ifNoSuperBlock404,
verifyCert
);
router.get(
'/certification/:username/:cert',
showCert
);
app.use(router);
const noNameMessage = dedent`
We need your name so we can put it on your certification.
Add your name to your account settings and click the save button.
Then we can issue your certification.
`;
const notCertifiedMessage = name => dedent`
it looks like you have not completed the necessary steps.
Please complete the required challenges to claim the
${name}
`;
const alreadyClaimedMessage = name => dedent`
It looks like you already have claimed the ${name}
`;
const successMessage = (username, name) => dedent`
@${username}, you have successfully claimed
the ${name}!
Congratulations on behalf of the freeCodeCamp team!
`;
function verifyCert(req, res, next) {
const { body: { superBlock }, user } = req;
log(superBlock);
let certType = superBlockCertTypeMap[superBlock];
log(certType);
return user.getCompletedChallenges$()
.flatMap(() => certTypeIds[certType])
.flatMap(challenge => {
const certName = certText[certType];
if (user[certType]) {
return Observable.just(alreadyClaimedMessage(certName));
}
let updateData = {
$set: {
[certType]: true
}
};
if (challenge) {
const {
id,
tests,
challengeType
} = challenge;
if (!user[certType] &&
!isCertified(tests, user.completedChallenges)) {
return Observable.just(notCertifiedMessage(certName));
}
updateData['$push'] = {
completedChallenges: {
id,
completedDate: new Date(),
challengeType
}
};
user.completedChallenges[
user.completedChallenges.length - 1
] = { id, completedDate: new Date() };
}
if (!user.name) {
return Observable.just(noNameMessage);
}
// set here so sendCertifiedEmail works properly
// not used otherwise
user[certType] = true;
return Observable.combineLatest(
// update user data
user.update$(updateData),
// 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$),
({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage })
)
.map(
({ count, pledgeOrMessage }) => {
if (typeof pledgeOrMessage === 'string') {
log(pledgeOrMessage);
}
log(`${count} documents updated`);
return successMessage(user.username, certName);
}
);
})
.subscribe(
(message) => {
return res.status(200).json({
message,
success: message.includes('Congratulations')
});
},
next
);
}
function ifNoSuperBlock404(req, res, next) {
const { superBlock } = req.body;
if (superBlock && superBlocks.includes(superBlock)) {
return next();
}
return res.status(404).end();
}
function showCert(req, res, next) {
let { username, cert } = req.params;
username = username.toLowerCase();
const certType = superBlockCertTypeMap[cert];
const certId = certIds[certType];
return findUserByUsername$(
username,
{
isCheater: true,
isFrontEndCert: true,
isBackEndCert: true,
isFullStackCert: true,
isRespWebDesignCert: true,
isFrontEndLibsCert: true,
isJsAlgoDataStructCert: true,
isDataVisCert: true,
is2018DataVisCert: true,
isApisMicroservicesCert: true,
isInfosecQaCert: true,
isHonest: true,
username: true,
name: true,
completedChallenges: true,
profileUI: true
}
)
.subscribe(
user => {
if (!user) {
req.flash(
'danger',
`We couldn't find a user with the username ${username}`
);
return res.redirect('/');
}
const { isLocked, showCerts } = user.profileUI;
const profile = `/portfolio/${user.username}`;
if (!user.name) {
req.flash(
'danger',
dedent`
This user needs to add their name to their account
in order for others to be able to view their certification.
`
);
return res.redirect(profile);
}
if (user.isCheater) {
return res.redirect(profile);
}
if (isLocked) {
req.flash(
'danger',
dedent`
${username} has chosen to make their profile
private. They will need to make their profile public
in order for others to be able to view their certification.
`
);
return res.redirect('/');
}
if (!showCerts) {
req.flash(
'danger',
dedent`
${username} has chosen to make their certifications
private. They will need to make their certifications public
in order for others to be able to view them.
`
);
return res.redirect('/');
}
if (!user.isHonest) {
req.flash(
'danger',
dedent`
${username} has not yet agreed to our Academic Honesty Pledge.
`
);
return res.redirect(profile);
}
if (user[certType]) {
const { completedChallenges = [] } = user;
const { completedDate = new Date() } = _.find(
completedChallenges, ({ id }) => certId === id
) || {};
return res.render(
certViews[certType],
{
username: user.username,
date: moment(new Date(completedDate)).format('MMMM D, YYYY'),
name: user.name
}
);
}
req.flash(
'danger',
`Looks like user ${username} is not ${certText[certType]} certified`
);
return res.redirect(profile);
},
next
);
}
}

View File

@ -0,0 +1,416 @@
/**
*
* Any ref to fixCompletedChallengesItem should be removed post
* a db migration to fix all completedChallenges
*
*/
import _ from 'lodash';
import debug from 'debug';
import accepts from 'accepts';
import dedent from 'dedent';
import { ifNoUserSend } from '../utils/middleware';
import { getChallengeById, cachedMap } from '../utils/map';
import { dasherize } from '../utils';
import pathMigrations from '../resources/pathMigration.json';
import { fixCompletedChallengeItem } from '../../common/utils';
const log = debug('fcc:boot:challenges');
const learnURL = 'https://learn.freecodecamp.org';
const jsProjects = [
'aaa48de84e1ecc7c742e1124',
'a7f4d8f2483413a6ce226cac',
'56533eb9ac21ba0edf2244e2',
'aff0395860f5d3034dc0bfc9',
'aa2e6f85cab2ab736c9a9b24'
];
function buildUserUpdate(
user,
challengeId,
_completedChallenge,
timezone
) {
const { files } = _completedChallenge;
let completedChallenge = {};
if (jsProjects.includes(challengeId)) {
completedChallenge = {
..._completedChallenge,
files: Object.keys(files)
.map(key => files[key])
.map(file => _.pick(
file,
[
'contents',
'key',
'index',
'name',
'path',
'ext'
]
))
};
} else {
completedChallenge = _.omit(_completedChallenge, ['files']);
}
let finalChallenge;
const updateData = {};
const { timezone: userTimezone, completedChallenges = [] } = user;
const oldChallenge = _.find(
completedChallenges,
({ id }) => challengeId === id
);
const alreadyCompleted = !!oldChallenge;
if (alreadyCompleted) {
finalChallenge = {
...completedChallenge,
completedDate: oldChallenge.completedDate
};
} else {
updateData.$push = {
...updateData.$push,
progressTimestamps: Date.now()
};
finalChallenge = {
...completedChallenge
};
}
updateData.$set = {
completedChallenges: _.uniqBy(
[finalChallenge, ...completedChallenges.map(fixCompletedChallengeItem)],
'id'
)
};
if (
timezone &&
timezone !== 'UTC' &&
(!userTimezone || userTimezone === 'UTC')
) {
updateData.$set = {
...updateData.$set,
timezone: userTimezone
};
}
log('user update data', updateData);
return {
alreadyCompleted,
updateData,
completedDate: finalChallenge.completedDate
};
}
export default function(app) {
const send200toNonUser = ifNoUserSend(true);
const api = app.loopback.Router();
const router = app.loopback.Router();
const map = cachedMap(app.models);
api.post(
'/modern-challenge-completed',
send200toNonUser,
modernChallengeCompleted
);
// deprecate endpoint
// remove once new endpoint is live
api.post(
'/completed-challenge',
send200toNonUser,
completedChallenge
);
api.post(
'/challenge-completed',
send200toNonUser,
completedChallenge
);
// deprecate endpoint
// remove once new endpoint is live
api.post(
'/completed-zipline-or-basejump',
send200toNonUser,
projectCompleted
);
api.post(
'/project-completed',
send200toNonUser,
projectCompleted
);
api.post(
'/backend-challenge-completed',
send200toNonUser,
backendChallengeCompleted
);
router.get(
'/challenges/current-challenge',
redirectToCurrentChallenge
);
router.get('/challenges', redirectToLearn);
router.get('/challenges/*', redirectToLearn);
router.get('/map', redirectToLearn);
app.use(api);
app.use('/external', api);
app.use('/internal', api);
app.use(router);
function modernChallengeCompleted(req, res, next) {
const type = accepts(req).type('html', 'json', 'text');
req.checkBody('id', 'id must be an ObjectId').isMongoId();
const errors = req.validationErrors(true);
if (errors) {
if (type === 'json') {
return res.status(403).send({ errors });
}
log('errors', errors);
return res.sendStatus(403);
}
const user = req.user;
return user.getCompletedChallenges$()
.flatMap(() => {
const completedDate = Date.now();
const {
id,
files
} = req.body;
const {
alreadyCompleted,
updateData
} = buildUserUpdate(
user,
id,
{ id, files, completedDate }
);
const points = alreadyCompleted ? user.points : user.points + 1;
return user.update$(updateData)
.doOnNext(() => user.manualReload())
.doOnNext(({ count }) => log('%s documents updated', count))
.map(() => {
if (type === 'json') {
return res.json({
points,
alreadyCompleted,
completedDate
});
}
return res.sendStatus(200);
});
})
.subscribe(() => {}, next);
}
function completedChallenge(req, res, next) {
req.checkBody('id', 'id must be an ObjectId').isMongoId();
const type = accepts(req).type('html', 'json', 'text');
const errors = req.validationErrors(true);
if (errors) {
if (type === 'json') {
return res.status(403).send({ errors });
}
log('errors', errors);
return res.sendStatus(403);
}
return req.user.getCompletedChallenges$()
.flatMap(() => {
const completedDate = Date.now();
const { id, solution, timezone, files } = req.body;
const {
alreadyCompleted,
updateData
} = buildUserUpdate(
req.user,
id,
{ id, solution, completedDate, files },
timezone
);
const user = req.user;
const points = alreadyCompleted ? user.points : user.points + 1;
return user.update$(updateData)
.doOnNext(({ count }) => log('%s documents updated', count))
.map(() => {
if (type === 'json') {
return res.json({
points,
alreadyCompleted,
completedDate
});
}
return res.sendStatus(200);
});
})
.subscribe(() => {}, next);
}
function projectCompleted(req, res, next) {
const type = accepts(req).type('html', 'json', 'text');
req.checkBody('id', 'id must be an ObjectId').isMongoId();
req.checkBody('challengeType', 'must be a number').isNumber();
req.checkBody('solution', 'solution must be a URL').isURL();
const errors = req.validationErrors(true);
if (errors) {
if (type === 'json') {
return res.status(403).send({ errors });
}
log('errors', errors);
return res.sendStatus(403);
}
const { user, body = {} } = req;
const completedChallenge = _.pick(
body,
[ 'id', 'solution', 'githubLink', 'challengeType', 'files' ]
);
completedChallenge.completedDate = Date.now();
if (
!completedChallenge.solution ||
// only basejumps require github links
(
completedChallenge.challengeType === 4 &&
!completedChallenge.githubLink
)
) {
req.flash(
'danger',
'You haven\'t supplied the necessary URLs for us to inspect your work.'
);
return res.sendStatus(403);
}
return user.getCompletedChallenges$()
.flatMap(() => {
const {
alreadyCompleted,
updateData
} = buildUserUpdate(user, completedChallenge.id, completedChallenge);
return user.update$(updateData)
.doOnNext(() => user.manualReload())
.doOnNext(({ count }) => log('%s documents updated', count))
.doOnNext(() => {
if (type === 'json') {
return res.send({
alreadyCompleted,
points: alreadyCompleted ? user.points : user.points + 1,
completedDate: completedChallenge.completedDate
});
}
return res.status(200).send(true);
});
})
.subscribe(() => {}, next);
}
function backendChallengeCompleted(req, res, next) {
const type = accepts(req).type('html', 'json', 'text');
req.checkBody('id', 'id must be an ObjectId').isMongoId();
req.checkBody('solution', 'solution must be a URL').isURL();
const errors = req.validationErrors(true);
if (errors) {
if (type === 'json') {
return res.status(403).send({ errors });
}
log('errors', errors);
return res.sendStatus(403);
}
const { user, body = {} } = req;
const completedChallenge = _.pick(
body,
[ 'id', 'solution' ]
);
completedChallenge.completedDate = Date.now();
return user.getCompletedChallenges$()
.flatMap(() => {
const {
alreadyCompleted,
updateData
} = buildUserUpdate(user, completedChallenge.id, completedChallenge);
return user.update$(updateData)
.doOnNext(({ count }) => log('%s documents updated', count))
.doOnNext(() => {
if (type === 'json') {
return res.send({
alreadyCompleted,
points: alreadyCompleted ? user.points : user.points + 1,
completedDate: completedChallenge.completedDate
});
}
return res.status(200).send(true);
});
})
.subscribe(() => {}, next);
}
function redirectToCurrentChallenge(req, res, next) {
const { user } = req;
const challengeId = user && user.currentChallengeId;
return getChallengeById(map, challengeId)
.map(challenge => {
const { block, dashedName, superBlock } = challenge;
if (!dashedName || !block) {
// this should normally not be hit if database is properly seeded
throw new Error(dedent`
Attempted to find '${dashedName}'
from '${ challengeId || 'no challenge id found'}'
but came up empty.
db may not be properly seeded.
`);
}
return `${learnURL}/${dasherize(superBlock)}/${block}/${dashedName}`;
})
.subscribe(
redirect => res.redirect(redirect || learnURL),
next
);
}
function redirectToLearn(req, res) {
const maybeChallenge = _.last(req.path.split('/'));
if (maybeChallenge in pathMigrations) {
const redirectPath = pathMigrations[maybeChallenge];
return res.status(302).redirect(`${learnURL}${redirectPath}`);
}
return res.status(302).redirect(learnURL);
}
}

View File

@ -0,0 +1,242 @@
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';
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

@ -0,0 +1,139 @@
import Stripe from 'stripe';
import keys from '../../../config/secrets';
export default function donateBoot(app, done) {
let stripe = false;
const { User } = app.models;
const api = app.loopback.Router();
const donateRouter = app.loopback.Router();
const subscriptionPlans = [500, 1000, 3500, 5000, 25000].reduce(
(accu, current) => ({
...accu,
[current]: {
amount: current,
interval: 'month',
product: {
name:
'Monthly Donation to freeCodeCamp.org - ' +
`Thank you ($${current / 100})`
},
currency: 'usd',
id: `monthly-donation-${current}`
}
}), {}
);
function connectToStripe() {
return new Promise(function(resolve) {
// connect to stripe API
stripe = Stripe(keys.stripe.secret);
// parse stripe plans
stripe.plans.list({}, function(err, plans) {
if (err) {
throw err;
}
const requiredPlans = Object.keys(subscriptionPlans).map(
key => subscriptionPlans[key].id
);
const availablePlans = plans.data.map(plan => plan.id);
requiredPlans.forEach(planId => {
if (!availablePlans.includes(planId)) {
const key = planId.split('-').slice(-1)[0];
createStripePlan(subscriptionPlans[key]);
}
});
});
resolve();
});
}
function createStripePlan(plan) {
stripe.plans.create(plan, function(err) {
if (err) {
console.log(err);
throw err;
}
console.log(`${plan.id} created`);
return;
});
}
function createStripeDonation(req, res) {
const { user, body } = req;
if (!body || !body.amount) {
return res.status(400).send({ error: 'Amount Required' });
}
const { amount, token: {email, id} } = body;
const fccUser = user ?
Promise.resolve(user) :
User.create$({ email }).toPromise();
let donatingUser = {};
let donation = {
email,
amount,
provider: 'stripe',
startDate: new Date(Date.now()).toISOString()
};
return fccUser.then(
user => {
donatingUser = user;
return stripe.customers
.create({
email,
card: id
});
})
.then(customer => {
donation.customerId = customer.id;
return stripe.subscriptions.create({
customer: customer.id,
items: [
{
plan: `monthly-donation-${amount}`
}
]
});
})
.then(subscription => {
donation.subscriptionId = subscription.id;
return res.send(subscription);
})
.then(() => {
donatingUser.createDonation(donation).toPromise()
.catch(err => {
throw new Error(err);
});
})
.catch(err => {
if (err.type === 'StripeCardError') {
return res.status(402).send({ error: err.message });
}
return res.status(500).send({ error: 'Donation Failed' });
});
}
const pubKey = keys.stripe.public;
const secKey = keys.stripe.secret;
const secretInvalid = !secKey || secKey === 'sk_from_stipe_dashboard';
const publicInvalid = !pubKey || pubKey === 'pk_from_stipe_dashboard';
if (secretInvalid || publicInvalid) {
if (process.env.NODE_ENV === 'production') {
throw new Error('Stripe API keys are required to boot the server!');
}
console.info('No Stripe API keys were found, moving on...');
done();
} else {
api.post('/charge-stripe', createStripeDonation);
donateRouter.use('/donate', api);
app.use(donateRouter);
app.use('/external', donateRouter);
connectToStripe().then(done);
}
}

View File

@ -0,0 +1,33 @@
const createDebugger = require('debug');
const log = createDebugger('fcc:boot:explorer');
module.exports = function mountLoopBackExplorer(app) {
if (process.env.NODE_ENV === 'production') {
return;
}
let explorer;
try {
explorer = require('loopback-component-explorer');
} catch (err) {
// Print the message only when the app was started via `app.listen()`.
// Do not print any message when the project is used as a component.
app.once('started', function() {
log(
'Run `npm install loopback-component-explorer` to enable ' +
'the LoopBack explorer'
);
});
return;
}
const restApiRoot = app.get('restApiRoot');
const mountPath = '/explorer';
explorer(app, { basePath: restApiRoot, mountPath });
app.once('started', function() {
const baseUrl = app.get('url').replace(/\/$/, '');
log('Browse your REST API at %s%s', baseUrl, mountPath);
});
};

View File

@ -0,0 +1,214 @@
// import React from 'react';
// import { renderToString } from 'react-dom/server';
// // import { StaticRouter } from 'react-router-dom';
// import { has } from 'lodash';
import debug from 'debug';
// import NewsApp from '../../news/NewsApp';
const routerLog = debug('fcc:boot:news:router');
const apiLog = debug('fcc:boot:news:api');
export default function newsBoot(app) {
// const router = app.loopback.Router();
// const api = app.loopback.Router();
// router.get('/n', (req, res) => res.redirect('/news'));
// router.get('/n/:shortId', createShortLinkHandler(app));
// router.get('/news', serveNewsApp);
// router.get('/news/*', serveNewsApp);
// api.post('/p', createPopularityHandler(app));
// app.use(api);
// app.use(router);
}
// function serveNewsApp(req, res) {
// const context = {};
// const markup = renderToString(
// <StaticRouter basename='/news' context={context} location={req.url}>
// <NewsApp />
// </StaticRouter>
// );
// if (context.url) {
// routerLog('redirect found in `renderToString`');
// // 'client-side' routing hit on a redirect
// return res.redirect(context.url);
// }
// routerLog('news markup sending');
// return res.render('layout-news', { title: 'News | freeCodeCamp', markup });
// }
// function createShortLinkHandler(app) {
// const { Article } = app.models;
// const referralHandler = createRerralHandler(app);
// return function shortLinkHandler(req, res, next) {
// const { query, user } = req;
// const { shortId } = req.params;
// referralHandler(query, shortId, !!user);
// routerLog(req.origin);
// routerLog(query.refsource);
// if (!shortId) {
// return res.redirect('/news');
// }
// routerLog('shortId', shortId);
// return Article.findOne(
// {
// where: {
// or: [{ shortId }, { slugPart: shortId }]
// }
// },
// (err, article) => {
// if (err) {
// next(err);
// }
// if (!article) {
// return res.redirect('/news');
// }
// const {
// slugPart,
// shortId,
// author: { username }
// } = article;
// const slug = `/news/${username}/${slugPart}--${shortId}`;
// return res.redirect(slug);
// }
// );
// };
// }
// function createPopularityHandler(app) {
// const { Article, Popularity } = app.models;
// return function handlePopularityStats(req, res, next) {
// const { body, user } = req;
// if (
// !has(body, 'event') ||
// !has(body, 'timestamp') ||
// !has(body, 'shortId')
// ) {
// console.warn('Popularity event recieved from client is malformed');
// console.log(JSON.stringify(body, null, 2));
// // sending 200 because the client shouldn't care for this
// return res.sendStatus(200);
// }
// res.sendStatus(200);
// const { shortId } = body;
// apiLog('shortId', shortId);
// const populartiyUpdate = {
// ...body,
// byAuthenticatedUser: !!user
// };
// Popularity.findOne({ where: { articleId: shortId } }, (err, popularity) => {
// if (err) {
// apiLog(err);
// return next(err);
// }
// if (popularity) {
// return popularity.updateAttribute(
// 'events',
// [populartiyUpdate, ...popularity.events],
// err => {
// if (err) {
// apiLog(err);
// return next(err);
// }
// return apiLog('poplarity updated');
// }
// );
// }
// return Popularity.create(
// {
// events: [populartiyUpdate],
// articleId: shortId
// },
// err => {
// if (err) {
// apiLog(err);
// return next(err);
// }
// return apiLog('poulartiy created');
// }
// );
// });
// return body.event === 'view'
// ? Article.findOne({ where: { shortId } }, (err, article) => {
// if (err) {
// apiLog(err);
// next(err);
// }
// return article.updateAttributes(
// { viewCount: article.viewCount + 1 },
// err => {
// if (err) {
// apiLog(err);
// return next(err);
// }
// return apiLog('article views updated');
// }
// );
// })
// : null;
// };
// }
// function createRerralHandler(app) {
// const { Popularity } = app.models;
// return function referralHandler(query, shortId, byAuthenticatedUser) {
// if (!query.refsource) {
// return null;
// }
// const eventUpdate = {
// event: `referral - ${query.refsource}`,
// timestamp: new Date(Date.now()),
// byAuthenticatedUser
// };
// return Popularity.findOne(
// { where: { articleId: shortId } },
// (err, popularity) => {
// if (err) {
// console.error(
// 'Failed finding a `Popularity` in a referral handler',
// err
// );
// return null;
// }
// if (popularity) {
// return popularity.updateAttribute(
// 'events',
// [eventUpdate, ...popularity.events],
// err => {
// if (err) {
// console.error(
// 'Failed in updating the `events` attribute of a `popularity`',
// err
// );
// }
// }
// );
// }
// return Popularity.create(
// {
// events: [eventUpdate],
// articleId: shortId
// },
// err => {
// if (err) {
// return console.error('Failed creating a new `Popularity`', err);
// }
// return apiLog('poulartiy created');
// }
// );
// }
// );
// };
// }

View File

@ -0,0 +1,304 @@
import request from 'request';
import constantStrings from '../utils/constantStrings.json';
import testimonials from '../resources/testimonials.json';
const githubClient = process.env.GITHUB_ID;
const githubSecret = process.env.GITHUB_SECRET;
module.exports = function(app) {
const router = app.loopback.Router();
const User = app.models.User;
router.get('/api/github', githubCalls);
router.get('/chat', chat);
router.get('/twitch', twitch);
router.get('/u/:email', unsubscribe);
router.get('/unsubscribe/:email', unsubscribe);
router.get('/ue/:unsubscribeId', unsubscribeById);
router.get(
'/the-fastest-web-page-on-the-internet',
theFastestWebPageOnTheInternet
);
router.get('/unsubscribed/:unsubscribeId', unsubscribedWithId);
router.get('/unsubscribed', unsubscribed);
router.get('/resubscribe/:unsubscribeId', resubscribe);
router.get('/nonprofits', nonprofits);
router.get('/nonprofits-form', nonprofitsForm);
router.get('/pmi-acp-agile-project-managers', agileProjectManagers);
router.get('/pmi-acp-agile-project-managers-form', agileProjectManagersForm);
router.get('/coding-bootcamp-cost-calculator', bootcampCalculator);
router.get('/stories', showTestimonials);
router.get('/all-stories', showAllTestimonials);
router.get('/how-nonprofit-projects-work', howNonprofitProjectsWork);
router.get(
'/software-resources-for-nonprofits',
softwareResourcesForNonprofits
);
router.get('/academic-honesty', academicHonesty);
app.use(router);
function chat(req, res) {
res.redirect('https://gitter.im/FreeCodeCamp/FreeCodeCamp');
}
function howNonprofitProjectsWork(req, res) {
res.redirect(301,
'https://medium.freecodecamp.com/open-source-for-good-1a0ea9f32d5a');
}
function softwareResourcesForNonprofits(req, res) {
res.render('resources/software-resources-for-nonprofits', {
title: 'Software Resources for Nonprofits'
});
}
function academicHonesty(req, res) {
res.render('resources/academic-honesty', {
title: 'Academic Honesty policy'
});
}
function theFastestWebPageOnTheInternet(req, res) {
res.render('resources/the-fastest-web-page-on-the-internet', {
title: 'This is the fastest web page on the internet'
});
}
function showTestimonials(req, res) {
res.render('resources/stories', {
title: 'Testimonials from Happy freeCodeCamp Students ' +
'who got Software Engineer Jobs',
stories: testimonials.slice(0, 72),
moreStories: true
});
}
function showAllTestimonials(req, res) {
res.render('resources/stories', {
title: 'Testimonials from Happy freeCodeCamp Students ' +
'who got Software Engineer Jobs',
stories: testimonials,
moreStories: false
});
}
function bootcampCalculator(req, res) {
res.render('resources/calculator', {
title: 'Coding Bootcamp Cost Calculator'
});
}
function nonprofits(req, res) {
res.render('resources/nonprofits', {
title: 'Your Nonprofit Can Get Pro Bono Code'
});
}
function nonprofitsForm(req, res) {
res.render('resources/nonprofits-form', {
title: 'Nonprofit Projects Proposal Form'
});
}
function agileProjectManagers(req, res) {
res.render('resources/pmi-acp-agile-project-managers', {
title: 'Get Agile Project Management Experience for the PMI-ACP'
});
}
function agileProjectManagersForm(req, res) {
res.render('resources/pmi-acp-agile-project-managers-form', {
title: 'Agile Project Management Program Application Form'
});
}
function twitch(req, res) {
res.redirect('https://twitch.tv/freecodecamp');
}
function unsubscribe(req, res, next) {
req.checkParams(
'email',
`"${req.params.email}" isn't a valid email address.`
).isEmail();
const errors = req.validationErrors(true);
if (errors) {
req.flash('error', { msg: errors.email.msg });
return res.redirect('/');
}
return User.find({
where: {
email: req.params.email
}
}, (err, users) => {
if (err) { return next(err); }
if (!users.length) {
req.flash('info', {
msg: 'Email address not found. Please update your Email ' +
'preferences from your settings.'
});
return res.redirect('/');
}
const updates = users.map(user => {
return new Promise((resolve, reject) =>
user.updateAttributes({
sendQuincyEmail: false
}, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
})
);
});
return Promise.all(updates)
.then(() => {
req.flash('info', {
msg: 'We\'ve successfully updated your Email preferences.'
});
return res.redirect('/unsubscribed');
})
.catch(next);
});
}
function unsubscribeById(req, res, next) {
const { unsubscribeId } = req.params;
if (!unsubscribeId) {
req.flash('info', {
msg: 'We could not find an account to unsubscribe'
});
return res.redirect('/');
}
return User.find({ where: { unsubscribeId } }, (err, users) => {
if (err || !users.length) {
req.flash('info', {
msg: 'We could not find an account to unsubscribe'
});
return res.redirect('/');
}
const updates = users.map(user => {
return new Promise((resolve, reject) =>
user.updateAttributes({
sendQuincyEmail: false
}, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
})
);
});
return Promise.all(updates)
.then(() => {
req.flash('success', {
msg: 'We\'ve successfully updated your email preferences.'
});
return res.redirect(`/unsubscribed/${unsubscribeId}`);
})
.catch(next);
});
}
function unsubscribed(req, res) {
res.render('resources/unsubscribed', {
title: 'You have been unsubscribed'
});
}
function unsubscribedWithId(req, res) {
const { unsubscribeId } = req.params;
return res.render('resources/unsubscribed', {
title: 'You have been unsubscribed',
unsubscribeId
});
}
function resubscribe(req, res, next) {
const { unsubscribeId } = req.params;
return User.find({ where: { unsubscribeId } },
(err, users) => {
if (err || !users.length) {
req.flash('info', {
msg: 'We could not find an account to unsubscribe'
});
return res.redirect('/');
}
const [ user ] = users;
return new Promise((resolve, reject) =>
user.updateAttributes({
sendQuincyEmail: true
}, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
})
)
.then(() => {
req.flash('success', {
msg:
'We\'ve successfully updated your email preferences. Thank you ' +
'for resubscribing.'
});
return res.redirect('/');
})
.catch(next);
});
}
function githubCalls(req, res, next) {
var githubHeaders = {
headers: {
'User-Agent': constantStrings.gitHubUserAgent
},
port: 80
};
request(
[
'https://api.github.com/repos/freecodecamp/',
'freecodecamp/pulls?client_id=',
githubClient,
'&client_secret=',
githubSecret
].join(''),
githubHeaders,
function(err, status1, pulls) {
if (err) { return next(err); }
pulls = pulls ?
Object.keys(JSON.parse(pulls)).length :
'Can\'t connect to github';
return request(
[
'https://api.github.com/repos/freecodecamp/',
'freecodecamp/issues?client_id=',
githubClient,
'&client_secret=',
githubSecret
].join(''),
githubHeaders,
function(err, status2, issues) {
if (err) { return next(err); }
issues = ((pulls === parseInt(pulls, 10)) && issues) ?
Object.keys(JSON.parse(issues)).length - pulls :
"Can't connect to GitHub";
return res.send({
issues: issues,
pulls: pulls
});
}
);
}
);
}
};

View File

@ -0,0 +1,32 @@
module.exports = function(app) {
var router = app.loopback.Router();
router.get('/nonprofit-project-instructions', function(req, res) {
res.redirect(
301,
'http://forum.freecodecamp.org/t/'
+ 'how-free-code-camps-nonprofits-projects-work/19547'
);
});
router.get('/agile', function(req, res) {
res.redirect(301, '/pmi-acp-agile-project-managers');
});
router.get('/privacy', function(req, res) {
res.redirect(
301,
'http://forum.freecodecamp.org/t/free-code-camp-privacy-policy/19545'
);
});
router.get('/learn-to-code', function(req, res) {
res.redirect(301, '/map');
});
router.get('/field-guide/*', function(req, res) {
res.redirect(302, 'http://forum.freecodecamp.org');
});
app.use(router);
};

View File

@ -0,0 +1,4 @@
module.exports = function mountRestApi(app) {
var restApiRoot = app.get('restApiRoot');
app.use(restApiRoot, app.loopback.rest());
};

View File

@ -0,0 +1,214 @@
import { check } from 'express-validator/check';
import { ifNoUser401, createValidatorErrorHandler } from '../utils/middleware';
import { themes } from '../../common/utils/themes.js';
import { alertTypes } from '../../common/utils/flash.js';
export default function settingsController(app) {
const api = app.loopback.Router();
const toggleUserFlag = (flag, req, res, next) => {
const { user } = req;
const currentValue = user[flag];
return user.update$({ [flag]: !currentValue }).subscribe(
() =>
res.status(200).json({
flag,
value: !currentValue
}),
next
);
};
function refetchCompletedChallenges(req, res, next) {
const { user } = req;
return user
.requestCompletedChallenges()
.subscribe(completedChallenges => res.json({ completedChallenges }), next);
}
const updateMyEmailValidators = [
check('email')
.isEmail()
.withMessage('Email format is invalid.')
];
function updateMyEmail(req, res, next) {
const {
user,
body: { email }
} = req;
return user
.requestUpdateEmail(email)
.subscribe(message => res.json({ message }), next);
}
const updateMyCurrentChallengeValidators = [
check('currentChallengeId')
.isMongoId()
.withMessage('currentChallengeId is not a valid challenge ID')
];
function updateMyCurrentChallenge(req, res, next) {
const {
user,
body: { currentChallengeId }
} = req;
return user.update$({ currentChallengeId }).subscribe(
() =>
res.json({
message: `your current challenge has been updated to ${currentChallengeId}`
}),
next
);
}
const updateMyThemeValidators = [
check('theme')
.isIn(Object.keys(themes))
.withMessage('Theme is invalid.')
];
function updateMyTheme(req, res, next) {
const {
body: { theme }
} = req;
if (req.user.theme === theme) {
return res.sendFlash(alertTypes.info, 'Theme already set');
}
return req.user
.updateTheme(theme)
.then(
() => res.sendFlash(alertTypes.info, 'Your theme has been updated'),
next
);
}
function updateFlags(req, res, next) {
const {
user,
body: { values }
} = req;
const keys = Object.keys(values);
if (keys.length === 1 && typeof keys[0] === 'boolean') {
return toggleUserFlag(keys[0], req, res, next);
}
return user
.requestUpdateFlags(values)
.subscribe(message => res.json({ message }), next);
}
function updateMyPortfolio(req, res, next) {
const {
user,
body: { portfolio }
} = req;
// if we only have one key, it should be the id
// user cannot send only one key to this route
// other than to remove a portfolio item
const requestDelete = Object.keys(portfolio).length === 1;
return user
.updateMyPortfolio(portfolio, requestDelete)
.subscribe(message => res.json({ message }), next);
}
function updateMyProfileUI(req, res, next) {
const {
user,
body: { profileUI }
} = req;
return user
.updateMyProfileUI(profileUI)
.subscribe(message => res.json({ message }), next);
}
function updateMyProjects(req, res, next) {
const {
user,
body: { projects: project }
} = req;
return user
.updateMyProjects(project)
.subscribe(message => res.json({ message }), next);
}
function updateMyUsername(req, res, next) {
const {
user,
body: { username }
} = req;
return user
.updateMyUsername(username)
.subscribe(message => res.json({ message }), next);
}
const updatePrivacyTerms = (req, res) => {
const {
user,
body: { quincyEmails }
} = req;
const update = {
acceptedPrivacyTerms: true,
sendQuincyEmail: !!quincyEmails
};
return user.updateAttributes(update, err => {
if (err) {
return res.status(500).json({
type: 'warning',
message:
'Something went wrong updating your preferences. ' +
'Please try again.'
});
}
return res.status(200).json({
type: 'success',
message:
'We have updated your preferences. ' +
'You can now continue using freeCodeCamp.'
});
});
};
api.put('/update-privacy-terms', ifNoUser401, updatePrivacyTerms);
api.post(
'/refetch-user-completed-challenges',
ifNoUser401,
refetchCompletedChallenges
);
api.post('/update-flags', ifNoUser401, updateFlags);
api.put(
'/update-my-email',
ifNoUser401,
updateMyEmailValidators,
createValidatorErrorHandler(alertTypes.danger),
updateMyEmail
);
api.post(
'/update-my-current-challenge',
ifNoUser401,
updateMyCurrentChallengeValidators,
createValidatorErrorHandler(alertTypes.danger),
updateMyCurrentChallenge
);
api.post(
'/update-my-current-challenge',
ifNoUser401,
updateMyCurrentChallengeValidators,
createValidatorErrorHandler(alertTypes.danger),
updateMyCurrentChallenge
);
api.post('/update-my-portfolio', ifNoUser401, updateMyPortfolio);
api.post('/update-my-profile-ui', ifNoUser401, updateMyProfileUI);
api.post('/update-my-projects', ifNoUser401, updateMyProjects);
api.post(
'/update-my-theme',
ifNoUser401,
updateMyThemeValidators,
createValidatorErrorHandler(alertTypes.danger),
updateMyTheme
);
api.post('/update-my-username', ifNoUser401, updateMyUsername);
app.use('/external', api);
app.use('/internal', api);
app.use(api);
}

View File

@ -0,0 +1,61 @@
import moment from 'moment';
import { Scheduler, Observable } from 'rx';
import { timeCache, observeQuery } from '../utils/rx';
import { dasherize } from '../utils';
const cacheTimeout = [ 24, 'hours' ];
const appUrl = 'https://www.freecodecamp.org';
// getCachedObservable(
// app: ExpressApp,
// modelName: String,
// nameProp: String,
// blockProp: String,
// map: (nameProp: String) => String
// ) => Observable[models]
function getCachedObservable(app, modelName, nameProp, blockProp, map) {
return observeQuery(
app.models[modelName],
'find',
{ fields: { [nameProp]: true, [blockProp]: true } }
)
.flatMap(models => {
return Observable.from(models, null, null, Scheduler.default);
})
.filter(model => !!model[nameProp] && !!model[blockProp])
.map(map ? map : (x) => x)
.toArray()
::timeCache(cacheTimeout[0], cacheTimeout[1]);
}
export default function sitemapRouter(app) {
const router = app.loopback.Router();
const challengeProps = ['dashedName', 'block'];
const challenges$ = getCachedObservable(app, 'Challenge', ...challengeProps);
const stories$ = getCachedObservable(app, 'Story', 'storyLink', dasherize);
function sitemap(req, res, next) {
const now = moment(new Date()).format('YYYY-MM-DD');
return Observable.combineLatest(
challenges$,
stories$,
(
challenges,
stories,
) => ({ challenges, stories })
)
.subscribe(
({ challenges, stories }) => {
res.header('Content-Type', 'application/xml');
res.render('resources/sitemap', {
appUrl,
now,
challenges,
stories
});
},
next
);
}
router.get('/sitemap.xml', sitemap);
app.use(router);
}

View File

@ -0,0 +1,6 @@
export default function bootStatus(app) {
const api = app.loopback.Router();
api.get('/status/ping', (req, res) => res.json({msg: 'pong'}));
app.use(api);
}

View File

@ -0,0 +1,12 @@
module.exports = function(app) {
var router = app.loopback.Router();
router.get('/wiki/*', showForum);
app.use(router);
function showForum(req, res) {
res.redirect(
'http://forum.freecodecamp.org/'
);
}
};

View File

@ -0,0 +1,265 @@
import dedent from 'dedent';
import debugFactory from 'debug';
import { curry, pick } from 'lodash';
import { Observable } from 'rx';
import {
getProgress,
normaliseUserFields,
userPropsForSession
} from '../utils/publicUserProps';
import { fixCompletedChallengeItem } from '../../common/utils';
import {
ifNoUser401,
ifNoUserRedirectTo,
ifNotVerifiedRedirectToUpdateEmail
} from '../utils/middleware';
const log = debugFactory('fcc:boot:user');
const sendNonUserToHome = ifNoUserRedirectTo('/');
const sendNonUserToHomeWithMessage = curry(ifNoUserRedirectTo, 2)('/');
module.exports = function bootUser(app) {
const router = app.loopback.Router();
const api = app.loopback.Router();
api.get('/account', sendNonUserToHome, getAccount);
api.get('/account/unlink/:social', sendNonUserToHome, getUnlinkSocial);
api.get('/user/get-session-user', readSessionUser);
api.post('/account/delete', ifNoUser401, createPostDeleteAccount(app));
api.post('/account/reset-progress', ifNoUser401, postResetProgress);
api.post(
'/user/:username/report-user/',
ifNoUser401,
createPostReportUserProfile(app)
);
router.get(
'/user/:username/report-user/',
sendNonUserToHomeWithMessage('You must be signed in to report a user'),
ifNotVerifiedRedirectToUpdateEmail,
getReportUserProfile
);
app.use(router);
app.use('/external', api);
app.use('/internal', api);
};
function readSessionUser(req, res, next) {
const queryUser = req.user;
const source =
queryUser &&
Observable.forkJoin(
queryUser.getCompletedChallenges$(),
queryUser.getPoints$(),
(completedChallenges, progressTimestamps) => ({
completedChallenges,
progress: getProgress(progressTimestamps, queryUser.timezone)
})
);
Observable.if(
() => !queryUser,
Observable.of({ user: {}, result: '' }),
Observable.defer(() => source)
.map(({ completedChallenges, progress }) => ({
...queryUser.toJSON(),
...progress,
completedChallenges: completedChallenges.map(fixCompletedChallengeItem)
}))
.map(user => ({
user: {
[user.username]: {
...pick(user, userPropsForSession),
isEmailVerified: !!user.emailVerified,
isGithub: !!user.githubProfile,
isLinkedIn: !!user.linkedin,
isTwitter: !!user.twitter,
isWebsite: !!user.website,
...normaliseUserFields(user)
}
},
result: user.username
}))
).subscribe(user => res.json(user), next);
}
function getReportUserProfile(req, res) {
const username = req.params.username.toLowerCase();
return res.render('account/report-profile', {
title: 'Report User',
username
});
}
function getAccount(req, res) {
const { username } = req.user;
return res.redirect('/' + username);
}
function getUnlinkSocial(req, res, next) {
const { user } = req;
const { username } = user;
let social = req.params.social;
if (!social) {
req.flash('danger', 'No social account found');
return res.redirect('/' + username);
}
social = social.toLowerCase();
const validSocialAccounts = ['twitter', 'linkedin'];
if (validSocialAccounts.indexOf(social) === -1) {
req.flash('danger', 'Invalid social account');
return res.redirect('/' + username);
}
if (!user[social]) {
req.flash('danger', `No ${social} account associated`);
return res.redirect('/' + username);
}
const query = {
where: {
provider: social
}
};
return user.identities(query, function(err, identities) {
if (err) {
return next(err);
}
// assumed user identity is unique by provider
let identity = identities.shift();
if (!identity) {
req.flash('danger', 'No social account found');
return res.redirect('/' + username);
}
return identity.destroy(function(err) {
if (err) {
return next(err);
}
const updateData = { [social]: null };
return user.update$(updateData).subscribe(() => {
log(`${social} has been unlinked successfully`);
req.flash('info', `You've successfully unlinked your ${social}.`);
return res.redirect('/' + username);
}, next);
});
});
}
function postResetProgress(req, res, next) {
const { user } = req;
user.updateAttributes(
{
progressTimestamps: [
{
timestamp: Date.now()
}
],
currentChallengeId: '',
isRespWebDesignCert: false,
is2018DataVisCert: false,
isFrontEndLibsCert: false,
isJsAlgoDataStructCert: false,
isApisMicroservicesCert: false,
isInfosecQaCert: false,
is2018FullStackCert: false,
isFrontEndCert: false,
isBackEndCert: false,
isDataVisCert: false,
isFullStackCert: false,
completedChallenges: []
},
function(err) {
if (err) {
return next(err);
}
return res.status(200).json({
messageType: 'success',
message: 'You have successfully reset your progress'
});
}
);
}
function createPostDeleteAccount(app) {
const { User } = app.models;
return function postDeleteAccount(req, res, next) {
User.destroyById(req.user.id, function(err) {
if (err) {
return next(err);
}
req.logout();
req.flash('success', 'You have successfully deleted your account.');
const config = {
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);
return res.status(200).end();
});
};
}
function createPostReportUserProfile(app) {
const { Email } = app.models;
return function postReportUserProfile(req, res, next) {
const { user } = req;
const { username } = req.params;
const report = req.sanitize('reportDescription').trimTags();
if (!username || !report || report === '') {
req.flash(
'danger',
'Oops, something is not right please re-check your submission.'
);
return next();
}
return Email.send$(
{
type: 'email',
to: 'team@freecodecamp.org',
cc: user.email,
from: 'team@freecodecamp.org',
subject: 'Abuse Report : Reporting ' + username + "'s profile.",
text: dedent(`
Hello Team,\n
This is to report the profile of ${username}.\n
Report Details:\n
${report}\n\n
Reported by:
Username: ${user.username}
Name: ${user.name}
Email: ${user.email}\n
Thanks and regards,
${user.name}
`)
},
err => {
if (err) {
err.redirectTo = '/' + username;
return next(err);
}
req.flash(
'info',
`A report was sent to the team with ${user.email} in copy.`
);
return res.redirect('/');
}
);
};
}

View File

@ -0,0 +1,118 @@
import debug from 'debug';
// import { renderToString } from 'react-dom/server';
// import createMemoryHistory from 'history/createMemoryHistory';
// import { NOT_FOUND } from 'redux-first-router';
// import devtoolsEnhancer from 'remote-redux-devtools';
// import {
// errorThrowerMiddleware
// } from '../utils/react.js';
// import { createApp, provideStore, App } from '../../common/app';
// import waitForEpics from '../../common/utils/wait-for-epics.js';
// import { titleSelector } from '../../common/app/redux';
const log = debug('fcc:react-server');
// const isDev = process.env.NODE_ENV !== 'production';
// // add routes here as they slowly get reactified
// // remove their individual controllers
// const routes = [
// '/settings',
// '/settings/*',
// '/:username'
// ];
// const devRoutes = [];
// const middlewares = isDev ? [errorThrowerMiddleware] : [];
// const markupMap = {};
export default function reactSubRouter(app) {
// var router = app.loopback.Router();
// router.get('/videos', (req, res) => res.redirect('/map'));
// router.get(
// '/videos/:dashedName',
// (req, res) => res.redirect(`/challenges/${req.params.dashedName}`)
// );
// router.get(
// '/portfolio/:redirectUsername',
// (req, res) => res.redirect(`/${req.params.redirectUsername}`)
// );
// // These routes are in production
// routes.forEach((route) => {
// router.get(route, serveReactApp);
// });
// if (process.env.NODE_ENV === 'development') {
// devRoutes.forEach(function(route) {
// router.get(route, serveReactApp);
// });
// }
// app.use(router);
// function serveReactApp(req, res, next) {
// const serviceOptions = { req };
// if (req.originalUrl in markupMap) {
// log('sending markup from cache');
// const { state, title, markup } = markupMap[req.originalUrl];
// res.expose(state, 'data', { isJSON: true });
// // note(berks): we render without express-flash dumping our messages
// // the app will query for these on load
// return res.renderWithoutFlash('layout-react', { markup, title });
// }
// return createApp({
// serviceOptions,
// middlewares,
// enhancers: [
// devtoolsEnhancer({ name: 'server' })
// ],
// history: createMemoryHistory({ initialEntries: [ req.originalUrl ] }),
// defaultState: {}
// })
// .filter(({
// location: {
// type,
// kind,
// pathname
// } = {}
// }) => {
// if (kind === 'redirect') {
// log('react found a redirect');
// res.redirect(pathname);
// return false;
// }
// if (type === NOT_FOUND) {
// log(`react tried to find ${req.path} but got 404`);
// next();
// return false;
// }
// return true;
// })
// .flatMap(({ store, epic }) => {
// return waitForEpics(epic)
// .map(() => renderToString(
// provideStore(App, store)
// ))
// .map((markup) => ({ markup, store, epic }));
// })
// .do(({ markup, store, epic }) => {
// log('react markup rendered, data fetched');
// const state = store.getState();
// const title = titleSelector(state);
// epic.dispose();
// res.expose(state, 'data', { isJSON: true });
// // note(berks): we render without express-flash dumping our messages
// // the app will query for these on load
// res.renderWithoutFlash('layout-react', { markup, title });
// markupMap[req.originalUrl] = { markup, state, title };
// })
// .subscribe(() => log('html rendered and sent'), next);
// }
}

View File

@ -0,0 +1,22 @@
import accepts from 'accepts';
export default function fourOhFour(app) {
app.all('*', function(req, res) {
const accept = accepts(req);
const type = accept.type('html', 'json', 'text');
const { path } = req;
if (type === 'html') {
req.flash('danger', `We couldn't find path ${ path }`);
return res.render('404', { title: '404'});
}
if (type === 'json') {
return res.status('404').json({ error: 'path not found' });
}
res.setHeader('Content-Type', 'text/plain');
return res.send('404 path not found');
});
}