chore(server): Move api-server in to it's own DIR
This commit is contained in:
committed by
mrugesh mohapatra
parent
9fba6bce4c
commit
46a217d0a5
15
api-server/server/boot/a-extend-built-ins.js
Normal file
15
api-server/server/boot/a-extend-built-ins.js
Normal 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
|
||||
);
|
||||
}
|
5
api-server/server/boot/a-increase-listeners.js
Normal file
5
api-server/server/boot/a-increase-listeners.js
Normal 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);
|
||||
};
|
20
api-server/server/boot/a-services.js
Normal file
20
api-server/server/boot/a-services.js
Normal 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);
|
||||
}
|
220
api-server/server/boot/authentication.js
Normal file
220
api-server/server/boot/authentication.js
Normal 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);
|
||||
};
|
450
api-server/server/boot/certificate.js
Normal file
450
api-server/server/boot/certificate.js
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
416
api-server/server/boot/challenge.js
Normal file
416
api-server/server/boot/challenge.js
Normal 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);
|
||||
}
|
||||
}
|
242
api-server/server/boot/commit.js
Normal file
242
api-server/server/boot/commit.js
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
139
api-server/server/boot/donate.js
Normal file
139
api-server/server/boot/donate.js
Normal 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);
|
||||
}
|
||||
}
|
33
api-server/server/boot/explorer.js
Normal file
33
api-server/server/boot/explorer.js
Normal 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);
|
||||
});
|
||||
};
|
214
api-server/server/boot/news.js
Normal file
214
api-server/server/boot/news.js
Normal 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');
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// );
|
||||
// };
|
||||
// }
|
304
api-server/server/boot/randomAPIs.js
Normal file
304
api-server/server/boot/randomAPIs.js
Normal 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
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
32
api-server/server/boot/redirects.js
Normal file
32
api-server/server/boot/redirects.js
Normal 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);
|
||||
};
|
4
api-server/server/boot/restApi.js
Normal file
4
api-server/server/boot/restApi.js
Normal file
@ -0,0 +1,4 @@
|
||||
module.exports = function mountRestApi(app) {
|
||||
var restApiRoot = app.get('restApiRoot');
|
||||
app.use(restApiRoot, app.loopback.rest());
|
||||
};
|
214
api-server/server/boot/settings.js
Normal file
214
api-server/server/boot/settings.js
Normal 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);
|
||||
}
|
61
api-server/server/boot/sitemap.js
Normal file
61
api-server/server/boot/sitemap.js
Normal 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);
|
||||
}
|
6
api-server/server/boot/status.js
Normal file
6
api-server/server/boot/status.js
Normal 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);
|
||||
}
|
12
api-server/server/boot/t-wiki.js
Normal file
12
api-server/server/boot/t-wiki.js
Normal 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/'
|
||||
);
|
||||
}
|
||||
};
|
265
api-server/server/boot/user.js
Normal file
265
api-server/server/boot/user.js
Normal 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('/');
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
118
api-server/server/boot/z-a-react.js
Normal file
118
api-server/server/boot/z-a-react.js
Normal 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);
|
||||
// }
|
||||
}
|
22
api-server/server/boot/z-not-found.js
Normal file
22
api-server/server/boot/z-not-found.js
Normal 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');
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user