chore(server): Move api-server in to it's own DIR
This commit is contained in:
committed by
mrugesh mohapatra
parent
9fba6bce4c
commit
46a217d0a5
5
api-server/server/README.md
Normal file
5
api-server/server/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
Everything to do with the server.
|
||||
|
||||
One file that is not tracked here is `rev-manifest.json`.
|
||||
It is generated at runtime and its contents change as the contents
|
||||
of client side files change.
|
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');
|
||||
});
|
||||
}
|
181
api-server/server/component-passport.js
Normal file
181
api-server/server/component-passport.js
Normal file
@ -0,0 +1,181 @@
|
||||
import passport from 'passport';
|
||||
import { PassportConfigurator } from
|
||||
'@freecodecamp/loopback-component-passport';
|
||||
import passportProviders from './passport-providers';
|
||||
import url from 'url';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import dedent from 'dedent';
|
||||
|
||||
const passportOptions = {
|
||||
emailOptional: true,
|
||||
profileToUser: null
|
||||
};
|
||||
|
||||
const fields = {
|
||||
progressTimestamps: false
|
||||
};
|
||||
|
||||
function getCompletedCertCount(user) {
|
||||
return [
|
||||
'isApisMicroservicesCert',
|
||||
'is2018DataVisCert',
|
||||
'isFrontEndLibsCert',
|
||||
'isInfosecQaCert',
|
||||
'isJsAlgoDataStructCert',
|
||||
'isRespWebDesignCert'
|
||||
].reduce((sum, key) => user[key] ? sum + 1 : sum, 0);
|
||||
}
|
||||
|
||||
function getLegacyCertCount(user) {
|
||||
return [
|
||||
'isFrontEndCert',
|
||||
'isBackEndCert',
|
||||
'isDataVisCert'
|
||||
].reduce((sum, key) => user[key] ? sum + 1 : sum, 0);
|
||||
}
|
||||
|
||||
PassportConfigurator.prototype.init = function passportInit(noSession) {
|
||||
this.app.middleware('session:after', passport.initialize());
|
||||
|
||||
if (noSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.app.middleware('session:after', passport.session());
|
||||
|
||||
// Serialization and deserialization is only required if passport session is
|
||||
// enabled
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id);
|
||||
});
|
||||
|
||||
passport.deserializeUser((id, done) => {
|
||||
|
||||
this.userModel.findById(id, { fields }, (err, user) => {
|
||||
if (err || !user) {
|
||||
return done(err, user);
|
||||
}
|
||||
|
||||
return this.app.dataSources.db.connector
|
||||
.collection('user')
|
||||
.aggregate([
|
||||
{ $match: { _id: user.id } },
|
||||
{ $project: { points: { $size: '$progressTimestamps' } } }
|
||||
], function(err, [{ points = 1 } = {}]) {
|
||||
if (err) { console.error(err); return done(err); }
|
||||
user.points = points;
|
||||
let completedChallengeCount = 0;
|
||||
let completedProjectCount = 0;
|
||||
if ('completedChallenges' in user) {
|
||||
completedChallengeCount = user.completedChallenges.length;
|
||||
user.completedChallenges.forEach(item => {
|
||||
if (
|
||||
'challengeType' in item &&
|
||||
(item.challengeType === 3 || item.challengeType === 4)
|
||||
) {
|
||||
completedProjectCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
user.completedChallengeCount = completedChallengeCount;
|
||||
user.completedProjectCount = completedProjectCount;
|
||||
user.completedCertCount = getCompletedCertCount(user);
|
||||
user.completedLegacyCertCount = getLegacyCertCount(user);
|
||||
user.completedChallenges = [];
|
||||
return done(null, user);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function setupPassport(app) {
|
||||
const configurator = new PassportConfigurator(app);
|
||||
|
||||
configurator.setupModels({
|
||||
userModel: app.models.user,
|
||||
userIdentityModel: app.models.userIdentity,
|
||||
userCredentialModel: app.models.userCredential
|
||||
});
|
||||
|
||||
configurator.init();
|
||||
|
||||
Object.keys(passportProviders).map(function(strategy) {
|
||||
let config = passportProviders[strategy];
|
||||
config.session = config.session !== false;
|
||||
|
||||
// https://stackoverflow.com/q/37430452
|
||||
let successRedirect = (req) => {
|
||||
if (!!req && req.session && req.session.returnTo) {
|
||||
delete req.session.returnTo;
|
||||
return '/';
|
||||
}
|
||||
return config.successRedirect || '';
|
||||
};
|
||||
|
||||
config.customCallback = !config.useCustomCallback
|
||||
? null
|
||||
: (req, res, next) => {
|
||||
|
||||
passport.authenticate(
|
||||
strategy,
|
||||
{ session: false },
|
||||
(err, user, userInfo) => {
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!user || !userInfo) {
|
||||
return res.redirect(config.failureRedirect);
|
||||
}
|
||||
let redirect = url.parse(successRedirect(req), true);
|
||||
|
||||
delete redirect.search;
|
||||
|
||||
const { accessToken } = userInfo;
|
||||
const { provider } = config;
|
||||
if (accessToken && accessToken.id) {
|
||||
if (provider === 'auth0') {
|
||||
req.flash(
|
||||
'success',
|
||||
dedent`
|
||||
Success! You have signed in to your account. Happy Coding!
|
||||
`
|
||||
);
|
||||
} else if (user.email) {
|
||||
req.flash(
|
||||
'info',
|
||||
dedent`
|
||||
We are moving away from social authentication for privacy reasons. Next time
|
||||
we recommend using your email address: ${user.email} to sign in instead.
|
||||
`
|
||||
);
|
||||
}
|
||||
const cookieConfig = {
|
||||
signed: !!req.signedCookies,
|
||||
maxAge: accessToken.ttl,
|
||||
domain: process.env.COOKIE_DOMAIN || 'localhost'
|
||||
};
|
||||
const jwtAccess = jwt.sign({accessToken}, process.env.JWT_SECRET);
|
||||
res.cookie('jwt_access_token', jwtAccess, cookieConfig);
|
||||
res.cookie('access_token', accessToken.id, cookieConfig);
|
||||
res.cookie('userId', accessToken.userId, cookieConfig);
|
||||
req.login(user);
|
||||
}
|
||||
|
||||
redirect = url.format(redirect);
|
||||
return res.redirect(redirect);
|
||||
}
|
||||
)(req, res, next);
|
||||
};
|
||||
|
||||
configurator.configureProvider(
|
||||
strategy,
|
||||
{
|
||||
...config,
|
||||
...passportOptions
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
9
api-server/server/config.development.js
Normal file
9
api-server/server/config.development.js
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
host: '127.0.0.1',
|
||||
sessionSecret: process.env.SESSION_SECRET,
|
||||
|
||||
github: {
|
||||
clientID: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET
|
||||
}
|
||||
};
|
22
api-server/server/config.json
Normal file
22
api-server/server/config.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"restApiRoot": "/api",
|
||||
"host": "127.0.0.1",
|
||||
"port": 3000,
|
||||
"legacyExplorer": false,
|
||||
"remoting": {
|
||||
"rest": {
|
||||
"handleErrors": false,
|
||||
"normalizeHttpPath": false,
|
||||
"xml": false
|
||||
},
|
||||
"json": {
|
||||
"strict": false,
|
||||
"limit": "100kb"
|
||||
},
|
||||
"urlencoded": {
|
||||
"extended": true,
|
||||
"limit": "100kb"
|
||||
},
|
||||
"cors": false
|
||||
}
|
||||
}
|
11
api-server/server/config.local.js
Normal file
11
api-server/server/config.local.js
Normal file
@ -0,0 +1,11 @@
|
||||
var globalConfig = require('../common/config.global');
|
||||
|
||||
module.exports = {
|
||||
restApiRoot: globalConfig.restApi,
|
||||
sessionSecret: process.env.SESSION_SECRET,
|
||||
|
||||
github: {
|
||||
clientID: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET
|
||||
}
|
||||
};
|
3
api-server/server/config.production.js
Normal file
3
api-server/server/config.production.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
host: process.env.HOST || 'localhost'
|
||||
};
|
30
api-server/server/datasources.development.js
Normal file
30
api-server/server/datasources.development.js
Normal file
@ -0,0 +1,30 @@
|
||||
const debug = require('debug')('fcc:server:datasources');
|
||||
const dsLocal = require('./datasources.production.js');
|
||||
|
||||
const ds = {
|
||||
...dsLocal
|
||||
};
|
||||
// use [MailHog](https://github.com/mailhog/MailHog) if no SES keys are found
|
||||
if (!process.env.SES_ID) {
|
||||
ds.mail = {
|
||||
connector: 'mail',
|
||||
transport: {
|
||||
type: 'smtp',
|
||||
host: process.env.MAILHOG_HOST || 'localhost',
|
||||
secure: false,
|
||||
port: 1025,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
},
|
||||
auth: {
|
||||
user: 'test',
|
||||
pass: 'test'
|
||||
}
|
||||
};
|
||||
debug(`using MailHog server on port ${ds.mail.transport.port}`);
|
||||
} else {
|
||||
debug('using AWS SES to deliver emails');
|
||||
}
|
||||
|
||||
module.exports = ds;
|
10
api-server/server/datasources.json
Normal file
10
api-server/server/datasources.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"db": {
|
||||
"name": "db",
|
||||
"connector": "mongodb"
|
||||
},
|
||||
"mail": {
|
||||
"name": "mail",
|
||||
"connector": "mail"
|
||||
}
|
||||
}
|
18
api-server/server/datasources.production.js
Normal file
18
api-server/server/datasources.production.js
Normal file
@ -0,0 +1,18 @@
|
||||
var secrets = require('../../config/secrets');
|
||||
|
||||
module.exports = {
|
||||
db: {
|
||||
connector: 'mongodb',
|
||||
connectionTimeout: 10000,
|
||||
url: secrets.db,
|
||||
useNewUrlParser: true
|
||||
},
|
||||
mail: {
|
||||
connector: 'mail',
|
||||
transport: {
|
||||
type: 'ses',
|
||||
accessKeyId: process.env.SES_ID,
|
||||
secretAccessKey: process.env.SES_SECRET
|
||||
}
|
||||
}
|
||||
};
|
6
api-server/server/debug-entry.js
Normal file
6
api-server/server/debug-entry.js
Normal file
@ -0,0 +1,6 @@
|
||||
// use this file with runners like node-debug
|
||||
// or mocha.
|
||||
require('babel-register');
|
||||
var app = require('./server');
|
||||
|
||||
app.start();
|
8
api-server/server/manifests/README.md
Normal file
8
api-server/server/manifests/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
This folder contains a list of json files representing the name
|
||||
of revisioned client files. It is empty due to the fact that the
|
||||
files are generated at runtime and their content is determined by
|
||||
the content of the files they are derived from.
|
||||
|
||||
Since the build process is not exactly the same on every machine,
|
||||
these files are not tracked in github otherwise conflicts arise when
|
||||
building on our servers.
|
74
api-server/server/middleware.json
Normal file
74
api-server/server/middleware.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"initial:before": {
|
||||
"loopback#favicon": {
|
||||
"params": "$!../public/favicon.ico"
|
||||
},
|
||||
"loopback#static": {
|
||||
"params": [
|
||||
"$!../public",
|
||||
{
|
||||
"maxAge": "86400000"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"initial": {
|
||||
"compression": {},
|
||||
"morgan": {
|
||||
"params": ":date[iso] :status :method :response-time ms - :url"
|
||||
},
|
||||
"cors": {
|
||||
"params": {
|
||||
"origin": true,
|
||||
"credentials": true,
|
||||
"maxAge": 86400
|
||||
}
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"./middlewares/sessions.js": {}
|
||||
},
|
||||
"auth:before": {
|
||||
"./middlewares/add-return-to": {},
|
||||
"./middlewares/cookie-parser": {},
|
||||
"./middlewares/jwt-authorization": {}
|
||||
},
|
||||
"parse": {
|
||||
"body-parser#json": {},
|
||||
"body-parser#urlencoded": {
|
||||
"params": {
|
||||
"extended": true
|
||||
}
|
||||
},
|
||||
"method-override": {}
|
||||
},
|
||||
"parse:after": {
|
||||
"./middlewares/validator": {}
|
||||
},
|
||||
"routes:before": {
|
||||
"./middlewares/express-extensions": {},
|
||||
"express-flash": {},
|
||||
"helmet#xssFilter": {},
|
||||
"helmet#noSniff": {},
|
||||
"helmet#frameguard": {},
|
||||
"./middlewares/csurf": {},
|
||||
"./middlewares/constant-headers": {},
|
||||
"./middlewares/csp": {},
|
||||
"./middlewares/jade-helpers": {},
|
||||
"./middlewares/flash-cheaters": {},
|
||||
"./middlewares/passport-login": {},
|
||||
"./middlewares/email-not-verified-notice": {},
|
||||
"./middlewares/privacy-terms-notice": {}
|
||||
},
|
||||
"files": {},
|
||||
"final:after": {
|
||||
"./middlewares/error-reporter": {},
|
||||
"./middlewares/error-handlers": {},
|
||||
"strong-error-handler": {
|
||||
"params": {
|
||||
"debug": false,
|
||||
"log": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
api-server/server/middlewares/add-return-to.js
Normal file
43
api-server/server/middlewares/add-return-to.js
Normal file
@ -0,0 +1,43 @@
|
||||
const pathsOfNoReturn = [
|
||||
'link',
|
||||
'auth',
|
||||
'login',
|
||||
'logout',
|
||||
'signin',
|
||||
'signup',
|
||||
'fonts',
|
||||
'favicon',
|
||||
'js',
|
||||
'css'
|
||||
];
|
||||
|
||||
const pathsWhiteList = [
|
||||
'news',
|
||||
'challenges',
|
||||
'map',
|
||||
'news',
|
||||
'commit'
|
||||
];
|
||||
|
||||
const pathsOfNoReturnRegex = new RegExp(pathsOfNoReturn.join('|'), 'i');
|
||||
const whiteListRegex = new RegExp(pathsWhiteList.join('|'), 'i');
|
||||
|
||||
export default function addReturnToUrl() {
|
||||
return function(req, res, next) {
|
||||
// Remember original destination before login.
|
||||
var path = req.path.split('/')[1];
|
||||
|
||||
if (
|
||||
req.method !== 'GET' ||
|
||||
pathsOfNoReturnRegex.test(path) ||
|
||||
!whiteListRegex.test(path) ||
|
||||
(/hot/i).test(req.path)
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
req.session.returnTo = req.originalUrl.includes('/map') ?
|
||||
'/' :
|
||||
req.originalUrl;
|
||||
return next();
|
||||
};
|
||||
}
|
9
api-server/server/middlewares/constant-headers.js
Normal file
9
api-server/server/middlewares/constant-headers.js
Normal file
@ -0,0 +1,9 @@
|
||||
export default function constantHeaders() {
|
||||
return function(req, res, next) {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Headers',
|
||||
'Origin, X-Requested-With, Content-Type, Accept'
|
||||
);
|
||||
next();
|
||||
};
|
||||
}
|
4
api-server/server/middlewares/cookie-parser.js
Normal file
4
api-server/server/middlewares/cookie-parser.js
Normal file
@ -0,0 +1,4 @@
|
||||
import cookieParser from 'cookie-parser';
|
||||
|
||||
const cookieSecret = process.env.COOKIE_SECRET;
|
||||
export default cookieParser.bind(cookieParser, cookieSecret);
|
98
api-server/server/middlewares/csp.js
Normal file
98
api-server/server/middlewares/csp.js
Normal file
@ -0,0 +1,98 @@
|
||||
import helmet from 'helmet';
|
||||
|
||||
let trusted = [
|
||||
"'self'",
|
||||
'https://search.freecodecamp.org',
|
||||
'https://www.freecodecamp.rocks',
|
||||
'https://api.freecodecamp.rocks',
|
||||
'https://' + process.env.AUTH0_DOMAIN
|
||||
];
|
||||
|
||||
const host = process.env.HOST || 'localhost';
|
||||
const port = process.env.SYNC_PORT || '3000';
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
trusted = trusted.concat([
|
||||
`ws://${host}:${port}`
|
||||
]);
|
||||
}
|
||||
|
||||
export default function csp() {
|
||||
return helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: trusted.concat([
|
||||
'https://*.cloudflare.com',
|
||||
'*.cloudflare.com'
|
||||
]),
|
||||
connectSrc: trusted.concat([
|
||||
'https://glitch.com',
|
||||
'https://*.glitch.com',
|
||||
'https://*.glitch.me',
|
||||
'https://*.cloudflare.com',
|
||||
'https://*.algolia.net'
|
||||
]),
|
||||
scriptSrc: [
|
||||
"'unsafe-eval'",
|
||||
"'unsafe-inline'",
|
||||
'*.google-analytics.com',
|
||||
'*.gstatic.com',
|
||||
'https://*.cloudflare.com',
|
||||
'*.cloudflare.com',
|
||||
'https://*.gitter.im',
|
||||
'https://*.cdnjs.com',
|
||||
'*.cdnjs.com',
|
||||
'https://*.jsdelivr.com',
|
||||
'*.jsdelivr.com',
|
||||
'*.twimg.com',
|
||||
'https://*.twimg.com',
|
||||
'*.youtube.com',
|
||||
'*.ytimg.com'
|
||||
].concat(trusted),
|
||||
styleSrc: [
|
||||
"'unsafe-inline'",
|
||||
'*.gstatic.com',
|
||||
'*.googleapis.com',
|
||||
'*.bootstrapcdn.com',
|
||||
'https://*.bootstrapcdn.com',
|
||||
'*.cloudflare.com',
|
||||
'https://*.cloudflare.com',
|
||||
'https://use.fontawesome.com'
|
||||
].concat(trusted),
|
||||
fontSrc: [
|
||||
'*.cloudflare.com',
|
||||
'https://*.cloudflare.com',
|
||||
'*.bootstrapcdn.com',
|
||||
'*.googleapis.com',
|
||||
'*.gstatic.com',
|
||||
'https://*.bootstrapcdn.com',
|
||||
'https://use.fontawesome.com'
|
||||
].concat(trusted),
|
||||
imgSrc: [
|
||||
// allow all input since we have user submitted images for
|
||||
// public profile
|
||||
'*',
|
||||
'data:'
|
||||
],
|
||||
mediaSrc: [
|
||||
'*.bitly.com',
|
||||
'*.amazonaws.com',
|
||||
'*.twitter.com'
|
||||
].concat(trusted),
|
||||
frameSrc: [
|
||||
'*.gitter.im',
|
||||
'*.gitter.im https:',
|
||||
'*.youtube.com',
|
||||
'*.twitter.com',
|
||||
'*.ghbtns.com',
|
||||
'*.freecatphotoapp.com',
|
||||
'freecodecamp.github.io'
|
||||
].concat(trusted)
|
||||
},
|
||||
// set to true if you only want to report errors
|
||||
reportOnly: false,
|
||||
// set to true if you want to set all headers
|
||||
setAllHeaders: false,
|
||||
// set to true if you want to force buggy CSP in Safari 5
|
||||
safari5: false
|
||||
});
|
||||
}
|
19
api-server/server/middlewares/csurf.js
Normal file
19
api-server/server/middlewares/csurf.js
Normal file
@ -0,0 +1,19 @@
|
||||
import csurf from 'csurf';
|
||||
|
||||
export default function() {
|
||||
const protection = csurf(
|
||||
{
|
||||
cookie: {
|
||||
domain: process.env.COOKIE_DOMAIN || 'localhost'
|
||||
}
|
||||
}
|
||||
);
|
||||
return function csrf(req, res, next) {
|
||||
|
||||
const path = req.path.split('/')[1];
|
||||
if (/(^api$|^external$|^internal$|^p$)/.test(path)) {
|
||||
return next();
|
||||
}
|
||||
return protection(req, res, next);
|
||||
};
|
||||
}
|
34
api-server/server/middlewares/email-not-verified-notice.js
Normal file
34
api-server/server/middlewares/email-not-verified-notice.js
Normal file
@ -0,0 +1,34 @@
|
||||
import dedent from 'dedent';
|
||||
|
||||
const ALLOWED_METHODS = ['GET'];
|
||||
const EXCLUDED_PATHS = [
|
||||
'/api/flyers/findOne',
|
||||
'/signout',
|
||||
'/accept-privacy-terms',
|
||||
'/update-email',
|
||||
'/confirm-email',
|
||||
'/passwordless-change',
|
||||
'/external/services/user'
|
||||
];
|
||||
|
||||
export default function emailNotVerifiedNotice() {
|
||||
return function(req, res, next) {
|
||||
if (
|
||||
ALLOWED_METHODS.indexOf(req.method) !== -1 &&
|
||||
EXCLUDED_PATHS.indexOf(req.path) === -1
|
||||
) {
|
||||
const { user } = req;
|
||||
if (user && (!user.email || user.email === '' || !user.emailVerified)) {
|
||||
req.flash(
|
||||
'info',
|
||||
dedent`
|
||||
New privacy laws now require that we have an email address where we can reach
|
||||
you. Please update your email address in the <a href='/settings'>settings</a>
|
||||
and click the link we send you to confirm.
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
return next();
|
||||
};
|
||||
}
|
90
api-server/server/middlewares/error-handlers.js
Normal file
90
api-server/server/middlewares/error-handlers.js
Normal file
@ -0,0 +1,90 @@
|
||||
// import { inspect } from 'util';
|
||||
// import _ from 'lodash/fp';
|
||||
import accepts from 'accepts';
|
||||
|
||||
import { homeLocation } from '../../../config/env';
|
||||
|
||||
import { unwrapHandledError } from '../utils/create-handled-error.js';
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
// const toString = Object.prototype.toString;
|
||||
// is full error or just trace
|
||||
// _.toString(new Error('foo')) => "Error: foo
|
||||
// Object.prototype.toString.call(new Error('foo')) => "[object Error]"
|
||||
// const isInspect = val => !val.stack && _.toString(val) === toString.call(val);
|
||||
// const stringifyErr = val => {
|
||||
// if (val.stack) {
|
||||
// return String(val.stack);
|
||||
// }
|
||||
|
||||
// const str = String(val);
|
||||
|
||||
// return isInspect(val) ?
|
||||
// inspect(val) :
|
||||
// str;
|
||||
// };
|
||||
|
||||
// const createStackHtml = _.flow(
|
||||
// _.cond([
|
||||
// [isInspect, err => [err]],
|
||||
// // may be stack or just err.msg
|
||||
// [_.stubTrue, _.flow(stringifyErr, _.split('\n'), _.tail) ]
|
||||
// ]),
|
||||
// _.map(_.escape),
|
||||
// _.map(line => `<li>${line}</lin>`),
|
||||
// _.join('')
|
||||
// );
|
||||
|
||||
// const createErrorTitle = _.cond([
|
||||
// [
|
||||
// _.negate(isInspect),
|
||||
// _.flow(stringifyErr, _.split('\n'), _.head, _.defaultTo('Error'))
|
||||
// ],
|
||||
// [_.stubTrue, _.constant('Error')]
|
||||
// ]);
|
||||
|
||||
export default function prodErrorHandler() {
|
||||
// error handling in production.
|
||||
// disabling eslint due to express parity rules for error handlers
|
||||
return function(err, req, res, next) {
|
||||
// eslint-disable-line
|
||||
const handled = unwrapHandledError(err);
|
||||
// respect handled error status
|
||||
let status = handled.status || err.status || res.statusCode;
|
||||
if (!handled.status && status < 400) {
|
||||
status = 500;
|
||||
}
|
||||
res.status(status);
|
||||
|
||||
// parse res type
|
||||
const accept = accepts(req);
|
||||
const type = accept.type('html', 'json', 'text');
|
||||
|
||||
const redirectTo = handled.redirectTo || `${homeLocation}/`;
|
||||
const message =
|
||||
handled.message || 'Oops! Something went wrong. Please try again later';
|
||||
|
||||
if (isDev) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (type === 'html') {
|
||||
if (typeof req.flash === 'function') {
|
||||
req.flash(handled.type || 'danger', message);
|
||||
}
|
||||
return res.redirectWithFlash(redirectTo);
|
||||
// json
|
||||
} else if (type === 'json') {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.send({
|
||||
type: handled.type || 'errors',
|
||||
message
|
||||
});
|
||||
// plain text
|
||||
} else {
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
return res.send(message);
|
||||
}
|
||||
};
|
||||
}
|
49
api-server/server/middlewares/error-reporter.js
Normal file
49
api-server/server/middlewares/error-reporter.js
Normal file
@ -0,0 +1,49 @@
|
||||
import debug from 'debug';
|
||||
import Rollbar from 'rollbar';
|
||||
import {
|
||||
isHandledError,
|
||||
unwrapHandledError
|
||||
} from '../utils/create-handled-error.js';
|
||||
|
||||
const { ROLLBAR_APP_ID } = process.env;
|
||||
|
||||
const rollbar = new Rollbar(ROLLBAR_APP_ID);
|
||||
const log = debug('fcc:middlewares:error-reporter');
|
||||
|
||||
const errTemplate = ({message, ...restError}, req) => `
|
||||
Time: ${new Date(Date.now()).toISOString()}
|
||||
Error: ${message}
|
||||
Is authenticated user: ${!!req.user}
|
||||
Route: ${JSON.stringify(req.route, null, 2)}
|
||||
|
||||
${JSON.stringify(restError, null, 2)}
|
||||
|
||||
`;
|
||||
|
||||
export default function errrorReporter() {
|
||||
if (process.env.NODE_ENV !== 'production' && process.env.ERROR_REPORTER) {
|
||||
return (err, req, res, next) => {
|
||||
console.error(errTemplate(err, req));
|
||||
|
||||
if (isHandledError(err)) {
|
||||
// log out user messages in development
|
||||
const handled = unwrapHandledError(err);
|
||||
log(handled.message);
|
||||
}
|
||||
next(err);
|
||||
};
|
||||
}
|
||||
return (err, req, res, next) => {
|
||||
// handled errors do not need to be reported,
|
||||
// they report a message and maybe redirect the user
|
||||
// errors with status codes shouldn't be reported
|
||||
// as they are usually user messages
|
||||
if (isHandledError(err) || err.statusCode || err.status) {
|
||||
return next(err);
|
||||
}
|
||||
// logging the error provides us with more information,
|
||||
// i.e isAuthenticatedUser, req.route
|
||||
console.error(errTemplate(err, req));
|
||||
return rollbar.error(err.message, err);
|
||||
};
|
||||
}
|
23
api-server/server/middlewares/express-extensions.js
Normal file
23
api-server/server/middlewares/express-extensions.js
Normal file
@ -0,0 +1,23 @@
|
||||
import qs from 'query-string';
|
||||
|
||||
// add rx methods to express
|
||||
export default function() {
|
||||
return function expressExtensions(req, res, next) {
|
||||
res.redirectWithFlash = uri => {
|
||||
const flash = req.flash();
|
||||
res.redirect(
|
||||
`${uri}?${qs.stringify(
|
||||
{ messages: qs.stringify(flash, { arrayFormat: 'index' }) },
|
||||
{ arrayFormat: 'index' }
|
||||
)}`
|
||||
);
|
||||
};
|
||||
res.sendFlash = (type, message) => {
|
||||
if (type && message) {
|
||||
req.flash(type, message);
|
||||
}
|
||||
return res.json(req.flash());
|
||||
};
|
||||
next();
|
||||
};
|
||||
}
|
30
api-server/server/middlewares/flash-cheaters.js
Normal file
30
api-server/server/middlewares/flash-cheaters.js
Normal file
@ -0,0 +1,30 @@
|
||||
import dedent from 'dedent';
|
||||
|
||||
const ALLOWED_METHODS = ['GET'];
|
||||
const EXCLUDED_PATHS = [
|
||||
'/api/flyers/findOne',
|
||||
'/challenges/current-challenge',
|
||||
'/challenges/next-challenge',
|
||||
'/map-aside',
|
||||
'/signout'
|
||||
];
|
||||
|
||||
export default function flashCheaters() {
|
||||
return function(req, res, next) {
|
||||
if (
|
||||
ALLOWED_METHODS.indexOf(req.method) !== -1 &&
|
||||
EXCLUDED_PATHS.indexOf(req.path) === -1 &&
|
||||
req.user && req.url !== '/' && req.user.isCheater
|
||||
) {
|
||||
req.flash(
|
||||
'danger',
|
||||
dedent`
|
||||
Upon review, this account has been flagged for academic
|
||||
dishonesty. If you’re the owner of this account contact
|
||||
team@freecodecamp.org for details.
|
||||
`
|
||||
);
|
||||
}
|
||||
return next();
|
||||
};
|
||||
}
|
59
api-server/server/middlewares/jade-helpers.js
Normal file
59
api-server/server/middlewares/jade-helpers.js
Normal file
@ -0,0 +1,59 @@
|
||||
import _ from 'lodash';
|
||||
import manifest from '../rev-manifest';
|
||||
|
||||
let chunkManifest;
|
||||
try {
|
||||
chunkManifest = require('../manifests/chunk-manifest.json');
|
||||
} catch (err) {
|
||||
chunkManifest = {};
|
||||
}
|
||||
|
||||
chunkManifest = Object.keys(chunkManifest).reduce((manifest, key) => {
|
||||
manifest[key] = '/' + chunkManifest[key];
|
||||
return manifest;
|
||||
}, {});
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint):\s/i;
|
||||
|
||||
function rev(scopedPrepend, asset) {
|
||||
if (isDev) {
|
||||
// do not use revision in dev mode
|
||||
return `${scopedPrepend}/${asset}`;
|
||||
}
|
||||
return `${scopedPrepend}/${ manifest[asset] || asset }`;
|
||||
}
|
||||
|
||||
function removeOldTerms(str = '') {
|
||||
return str.replace(challengesRegex, '');
|
||||
}
|
||||
|
||||
const cacheBreaker = isDev ?
|
||||
// add cacheBreaker in dev instead of rev manifest
|
||||
asset => `${asset}?cacheBreaker=${Math.random()}` :
|
||||
_.identity;
|
||||
|
||||
export default function jadeHelpers() {
|
||||
return function jadeHelpersMiddleware(req, res, next) {
|
||||
Object.assign(
|
||||
res.locals,
|
||||
{
|
||||
removeOldTerms,
|
||||
rev,
|
||||
cacheBreaker,
|
||||
// static data
|
||||
user: req.user,
|
||||
chunkManifest,
|
||||
_csrf: req.csrfToken ? req.csrfToken() : null,
|
||||
theme: req.user &&
|
||||
req.user.theme ||
|
||||
req.cookies.theme ||
|
||||
'default'
|
||||
}
|
||||
);
|
||||
if (req.csrfToken) {
|
||||
res.expose({ token: res.locals._csrf }, 'csrf');
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
69
api-server/server/middlewares/jwt-authorization.js
Normal file
69
api-server/server/middlewares/jwt-authorization.js
Normal file
@ -0,0 +1,69 @@
|
||||
import loopback from 'loopback';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { isBefore } from 'date-fns';
|
||||
|
||||
import { homeLocation } from '../../../config/env';
|
||||
|
||||
import { wrapHandledError } from '../utils/create-handled-error';
|
||||
|
||||
export default () => function authorizeByJWT(req, res, next) {
|
||||
const path = req.path.split('/')[1];
|
||||
if (/^external$|^internal$/.test(path)) {
|
||||
const cookie = req.signedCookies && req.signedCookies['jwt_access_token'] ||
|
||||
req.cookie && req.cookie['jwt_access_token'];
|
||||
if (!cookie) {
|
||||
throw wrapHandledError(
|
||||
new Error('Access token is required for this request'),
|
||||
{
|
||||
type: 'info',
|
||||
redirect: `${homeLocation}/signin`,
|
||||
message: 'Access token is required for this request',
|
||||
status: 403
|
||||
}
|
||||
);
|
||||
}
|
||||
let token;
|
||||
try {
|
||||
token = jwt.verify(cookie, process.env.JWT_SECRET);
|
||||
} catch (err) {
|
||||
throw wrapHandledError(
|
||||
new Error(err.message),
|
||||
{
|
||||
type: 'info',
|
||||
redirect: `${homeLocation}/signin`,
|
||||
message: 'Your access token is invalid',
|
||||
status: 403
|
||||
}
|
||||
);
|
||||
}
|
||||
const { accessToken: {created, ttl, userId }} = token;
|
||||
const valid = isBefore(Date.now(), Date.parse(created) + ttl);
|
||||
if (!valid) {
|
||||
throw wrapHandledError(
|
||||
new Error('Access token is no longer vaild'),
|
||||
{
|
||||
type: 'info',
|
||||
redirect: `${homeLocation}/signin`,
|
||||
message: 'Access token is no longer vaild',
|
||||
status: 403
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!req.user) {
|
||||
const User = loopback.getModelByType('User');
|
||||
return User.findById(userId)
|
||||
.then(user => {
|
||||
if (user) {
|
||||
user.points = user.progressTimestamps.length;
|
||||
req.user = user;
|
||||
}
|
||||
return;
|
||||
})
|
||||
.then(next)
|
||||
.catch(next);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
return next();
|
||||
};
|
21
api-server/server/middlewares/passport-login.js
Normal file
21
api-server/server/middlewares/passport-login.js
Normal file
@ -0,0 +1,21 @@
|
||||
import _ from 'lodash';
|
||||
import { Observable } from 'rx';
|
||||
import { login } from 'passport/lib/http/request';
|
||||
|
||||
// make login polymorphic
|
||||
// if supplied callback it works as normal
|
||||
// if called without callback it returns an observable
|
||||
// login(user, options?, cb?) => Void|Observable
|
||||
function login$(...args) {
|
||||
console.log('args');
|
||||
if (_.isFunction(_.last(args))) {
|
||||
return login.apply(this, args);
|
||||
}
|
||||
return Observable.fromNodeCallback(login).apply(this, args);
|
||||
}
|
||||
export default function passportLogin() {
|
||||
return (req, res, next) => {
|
||||
req.login = req.logIn = login$;
|
||||
next();
|
||||
};
|
||||
}
|
26
api-server/server/middlewares/privacy-terms-notice.js
Normal file
26
api-server/server/middlewares/privacy-terms-notice.js
Normal file
@ -0,0 +1,26 @@
|
||||
const ALLOWED_METHODS = ['GET'];
|
||||
const EXCLUDED_PATHS = [
|
||||
'/api/flyers/findOne',
|
||||
'/signout',
|
||||
'/accept-privacy-terms',
|
||||
'/update-email',
|
||||
'/confirm-email',
|
||||
'/passwordless-change',
|
||||
'/external/services/user'
|
||||
];
|
||||
|
||||
export default function privacyTermsNotAcceptedNotice() {
|
||||
return function(req, res, next) {
|
||||
if (
|
||||
ALLOWED_METHODS.indexOf(req.method) !== -1 &&
|
||||
EXCLUDED_PATHS.indexOf(req.path) === -1
|
||||
) {
|
||||
const { user } = req;
|
||||
if (user && user.acceptedPrivacyTerms !== true) {
|
||||
res.redirect('/accept-privacy-terms');
|
||||
return next;
|
||||
}
|
||||
}
|
||||
return next();
|
||||
};
|
||||
}
|
20
api-server/server/middlewares/sessions.js
Normal file
20
api-server/server/middlewares/sessions.js
Normal file
@ -0,0 +1,20 @@
|
||||
import session from 'express-session';
|
||||
import MongoStoreFactory from 'connect-mongo';
|
||||
|
||||
const MongoStore = MongoStoreFactory(session);
|
||||
const sessionSecret = process.env.SESSION_SECRET;
|
||||
const url = process.env.MONGODB || process.env.MONGOHQ_URL;
|
||||
|
||||
export default function sessionsMiddleware() {
|
||||
return session({
|
||||
// 900 day session cookie
|
||||
cookie: { maxAge: 900 * 24 * 60 * 60 * 1000 },
|
||||
// resave forces session to be resaved
|
||||
// regardless of whether it was modified
|
||||
// this causes race conditions during parallel req
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
secret: sessionSecret,
|
||||
store: new MongoStore({ url })
|
||||
});
|
||||
}
|
57
api-server/server/middlewares/validator.js
Normal file
57
api-server/server/middlewares/validator.js
Normal file
@ -0,0 +1,57 @@
|
||||
import validator from 'express-validator';
|
||||
import { isPoly } from '../../common/utils/polyvinyl';
|
||||
|
||||
const isObject = val => !!val && typeof val === 'object';
|
||||
|
||||
export default function() {
|
||||
return validator({
|
||||
customValidators: {
|
||||
matchRegex(param, regex) {
|
||||
return regex.test(param);
|
||||
},
|
||||
isString(value) {
|
||||
return typeof value === 'string';
|
||||
},
|
||||
isNumber(value) {
|
||||
return typeof value === 'number';
|
||||
},
|
||||
isFiles(value) {
|
||||
if (!isObject(value)) {
|
||||
return false;
|
||||
}
|
||||
const keys = Object.keys(value);
|
||||
return !!keys.length &&
|
||||
// every key is a file
|
||||
keys.every(key => isObject(value[key])) &&
|
||||
// every file has contents
|
||||
keys.map(key => value[key]).every(file => isPoly(file));
|
||||
}
|
||||
},
|
||||
customSanitizers: {
|
||||
// Refer : http://stackoverflow.com/a/430240/1932901
|
||||
trimTags(value) {
|
||||
const tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*';
|
||||
const tagOrComment = new RegExp(
|
||||
'<(?:'
|
||||
// Comment body.
|
||||
+ '!--(?:(?:-*[^->])*--+|-?)'
|
||||
// Special "raw text" elements whose content should be elided.
|
||||
+ '|script\\b' + tagBody + '>[\\s\\S]*?</script\\s*'
|
||||
+ '|style\\b' + tagBody + '>[\\s\\S]*?</style\\s*'
|
||||
// Regular name
|
||||
+ '|/?[a-z]'
|
||||
+ tagBody
|
||||
+ ')>',
|
||||
'gi'
|
||||
);
|
||||
let rawValue;
|
||||
do {
|
||||
rawValue = value;
|
||||
value = value.replace(tagOrComment, '');
|
||||
} while (value !== rawValue);
|
||||
|
||||
return value.replace(/</g, '<');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
98
api-server/server/model-config.json
Normal file
98
api-server/server/model-config.json
Normal file
@ -0,0 +1,98 @@
|
||||
{
|
||||
"_meta": {
|
||||
"sources": [
|
||||
"loopback/common/models",
|
||||
"loopback/server/models",
|
||||
"../common/models",
|
||||
"./models"
|
||||
]
|
||||
},
|
||||
"about": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"AuthToken": {
|
||||
"dataSource": "db",
|
||||
"public": false
|
||||
},
|
||||
"AccessToken": {
|
||||
"dataSource": "db",
|
||||
"public": false
|
||||
},
|
||||
"ACL": {
|
||||
"dataSource": "db",
|
||||
"public": false
|
||||
},
|
||||
"block": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"challenge": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"Donation": {
|
||||
"dataSource": "db",
|
||||
"public": false
|
||||
},
|
||||
"Email": {
|
||||
"dataSource": "mail",
|
||||
"public": false
|
||||
},
|
||||
"flyer": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"job": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"nonprofit": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"pledge": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"promo": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"Role": {
|
||||
"dataSource": "db",
|
||||
"public": false
|
||||
},
|
||||
"RoleMapping": {
|
||||
"dataSource": "db",
|
||||
"public": false
|
||||
},
|
||||
"story": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"userCredential": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"userIdentity": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"user": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"User": {
|
||||
"dataSource": "db",
|
||||
"public": false
|
||||
},
|
||||
"article": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"popularity": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
}
|
||||
}
|
30
api-server/server/models/about.js
Normal file
30
api-server/server/models/about.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { createActiveUsers } from '../utils/about.js';
|
||||
|
||||
|
||||
module.exports = function(About) {
|
||||
const activeUsers = createActiveUsers();
|
||||
let activeUsersForRendering = 0;
|
||||
About.getActiveUsers = async function getActiveUsers() {
|
||||
// converting to promise automatically will subscribe to Observable
|
||||
// initiating the sequence above
|
||||
const usersActive = await activeUsers.toPromise();
|
||||
activeUsersForRendering = usersActive;
|
||||
return usersActive;
|
||||
};
|
||||
|
||||
About.getActiveUsersForRendering = () => activeUsersForRendering;
|
||||
|
||||
About.remoteMethod(
|
||||
'getActiveUsers',
|
||||
{
|
||||
http: {
|
||||
path: '/get-active-users',
|
||||
verb: 'get'
|
||||
},
|
||||
returns: {
|
||||
type: 'number',
|
||||
arg: 'activeUsers'
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
28
api-server/server/models/about.json
Normal file
28
api-server/server/models/about.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "about",
|
||||
"plural": "about",
|
||||
"base": "PersistedModel",
|
||||
"idInjection": true,
|
||||
"options": {
|
||||
"validateUpsert": true
|
||||
},
|
||||
"properties": {},
|
||||
"validations": [],
|
||||
"relations": {},
|
||||
"acls": [
|
||||
{
|
||||
"accessType": "*",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY"
|
||||
},
|
||||
{
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW",
|
||||
"property": "getActiveUsers"
|
||||
}
|
||||
],
|
||||
"methods": {}
|
||||
}
|
15
api-server/server/models/auth-token.js
Normal file
15
api-server/server/models/auth-token.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { Observable } from 'rx';
|
||||
|
||||
export default function(AuthToken) {
|
||||
AuthToken.on('dataSourceAttached', () => {
|
||||
AuthToken.findOne$ = Observable.fromNodeCallback(
|
||||
AuthToken.findOne.bind(AuthToken)
|
||||
);
|
||||
AuthToken.prototype.validate$ = Observable.fromNodeCallback(
|
||||
AuthToken.prototype.validate
|
||||
);
|
||||
AuthToken.prototype.destroy$ = Observable.fromNodeCallback(
|
||||
AuthToken.prototype.destroy
|
||||
);
|
||||
});
|
||||
}
|
13
api-server/server/models/auth-token.json
Normal file
13
api-server/server/models/auth-token.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "AuthToken",
|
||||
"base": "AccessToken",
|
||||
"idInjection": true,
|
||||
"options": {
|
||||
"validateUpsert": true
|
||||
},
|
||||
"properties": {},
|
||||
"validations": [],
|
||||
"relations": {},
|
||||
"acls": [],
|
||||
"methods": {}
|
||||
}
|
15
api-server/server/models/donation.js
Normal file
15
api-server/server/models/donation.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { Observable } from 'rx';
|
||||
|
||||
export default function(Donation) {
|
||||
Donation.on('dataSourceAttached', () => {
|
||||
Donation.findOne$ = Observable.fromNodeCallback(
|
||||
Donation.findOne.bind(Donation)
|
||||
);
|
||||
Donation.prototype.validate$ = Observable.fromNodeCallback(
|
||||
Donation.prototype.validate
|
||||
);
|
||||
Donation.prototype.destroy$ = Observable.fromNodeCallback(
|
||||
Donation.prototype.destroy
|
||||
);
|
||||
});
|
||||
}
|
68
api-server/server/models/donation.json
Normal file
68
api-server/server/models/donation.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "Donation",
|
||||
"description": "A representaion of a donation to freeCodeCamp",
|
||||
"plural": "donations",
|
||||
"base": "PersistedModel",
|
||||
"idInjection": true,
|
||||
"scopes": {},
|
||||
"indexes": {},
|
||||
"options": {
|
||||
"validateUpsert": true
|
||||
},
|
||||
"hidden": [],
|
||||
"remoting": {},
|
||||
"http": {},
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "The email used to create the donation"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "The payment handler, paypal/stripe etc..."
|
||||
},
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"description": "The donation amount in cents"
|
||||
},
|
||||
"startDate": {
|
||||
"type": "DateString",
|
||||
"required": true
|
||||
},
|
||||
"endDate": {
|
||||
"type": "DateString"
|
||||
},
|
||||
"subscriptionId": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "The donation subscription id returned from the provider"
|
||||
},
|
||||
"customerId": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "The providers reference for the donator"
|
||||
}
|
||||
},
|
||||
"validations": [
|
||||
{
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"description": "Amount should be >= $1 (100c)",
|
||||
"min": 100
|
||||
},
|
||||
"facetName": "server"
|
||||
}
|
||||
],
|
||||
"relations": {
|
||||
"user": {
|
||||
"type": "belongsTo",
|
||||
"model": "user",
|
||||
"foreignKey": "userId"
|
||||
}
|
||||
},
|
||||
"acls": [],
|
||||
"methods": {}
|
||||
}
|
32
api-server/server/passport-providers.js
Normal file
32
api-server/server/passport-providers.js
Normal file
@ -0,0 +1,32 @@
|
||||
const successRedirect = '/';
|
||||
const failureRedirect = '/';
|
||||
|
||||
export default {
|
||||
local: {
|
||||
provider: 'local',
|
||||
module: 'passport-local',
|
||||
usernameField: 'email',
|
||||
passwordField: 'password',
|
||||
authPath: '/auth/local',
|
||||
successRedirect: successRedirect,
|
||||
failureRedirect: failureRedirect,
|
||||
session: true,
|
||||
failureFlash: true
|
||||
},
|
||||
'auth0-login': {
|
||||
provider: 'auth0',
|
||||
module: 'passport-auth0',
|
||||
clientID: process.env.AUTH0_CLIENT_ID,
|
||||
clientSecret: process.env.AUTH0_CLIENT_SECRET,
|
||||
domain: process.env.AUTH0_DOMAIN,
|
||||
cookieDomain: 'freeCodeCamp.org',
|
||||
callbackURL: '/auth/auth0/callback',
|
||||
authPath: '/auth/auth0',
|
||||
callbackPath: '/auth/auth0/callback',
|
||||
useCustomCallback: true,
|
||||
successRedirect: successRedirect,
|
||||
failureRedirect: failureRedirect,
|
||||
scope: ['openid profile email'],
|
||||
failureFlash: true
|
||||
}
|
||||
};
|
33
api-server/server/production-start.js
Normal file
33
api-server/server/production-start.js
Normal file
@ -0,0 +1,33 @@
|
||||
// this ensures node understands the future
|
||||
require('babel-register');
|
||||
const _ = require('lodash');
|
||||
const createDebugger = require('debug');
|
||||
|
||||
const log = createDebugger('fcc:server:production-start');
|
||||
const startTime = Date.now();
|
||||
// force logger to always output
|
||||
// this may be brittle
|
||||
log.enabled = true;
|
||||
// this is where server starts booting up
|
||||
const app = require('./server');
|
||||
|
||||
|
||||
let timeoutHandler;
|
||||
let killTime = 15;
|
||||
|
||||
const onConnect = _.once(() => {
|
||||
log('db connected in: %s', Date.now() - startTime);
|
||||
if (timeoutHandler) {
|
||||
clearTimeout(timeoutHandler);
|
||||
}
|
||||
app.start();
|
||||
});
|
||||
|
||||
timeoutHandler = setTimeout(() => {
|
||||
const message = `db did not connect after ${killTime}s -- crashing hard`;
|
||||
// purposely shutdown server
|
||||
// pm2 should restart this in production
|
||||
throw new Error(message);
|
||||
}, killTime * 1000);
|
||||
|
||||
app.dataSources.db.on('connected', onConnect);
|
569
api-server/server/resources/testimonials.json
Normal file
569
api-server/server/resources/testimonials.json
Normal file
@ -0,0 +1,569 @@
|
||||
[
|
||||
{
|
||||
"camper": "Meta Hirschl",
|
||||
"quote": "By building a robust and highly functional web app I was able to not only increase my confidence but was able to show potential employers what I was able to create. Both were huge for me and led me to getting a fantastic job.",
|
||||
"github": "MetaCoderHirschl",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=ADEAAAKX2fEBfeZ1GVdOh-c0zzkYZKw38o8qzow",
|
||||
"image": "https://i.imgur.com/nsvNixW.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Branden Byers",
|
||||
"quote": "My goal was to become employed by the end of 2015. Instead, I ended up with a job at the beginning of 2015. This was directly related to my work at freeCodeCamp.",
|
||||
"github": "brandenbyers",
|
||||
"linkedin": "https://www.linkedin.com/in/brandenbyers",
|
||||
"image": "https://i.imgur.com/NbzknHq.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Bruna Torman Reseres Franasa",
|
||||
"quote": "I’m now receiving offers for internships. I have no experience in IT, but now good things are happening!",
|
||||
"github": "brunatrf",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAABVPh9IB730qyshrsqO1hDNNRUL-X_4i8n0",
|
||||
"image": "https://i.imgur.com/TqxHSNY.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Maxim Orlov",
|
||||
"quote": "I started freeCodeCamp with zero knowledge of web development. 6 months later, I landed my first job as a back end engineer.",
|
||||
"github": "Maximization",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAo83nwBF57LpD9mZlm5dH6OcovOpYKPs3k",
|
||||
"image": "https://i.imgur.com/wjlDigg.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Alexander Black, Jr.",
|
||||
"quote": "My work on a nonprofit project gave me the opportunity to learn how to build Chrome extensions and showcase my skills as a full-stack developer.",
|
||||
"github": "alexblackjr",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAA553L4BgfgBR-M9RQc7x5matd6FUx3a6-I",
|
||||
"image": "https://i.imgur.com/iHC6ZI4.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Cristiane Henriques",
|
||||
"quote": "I am getting more work contacts after including freeCodeCamp on my CV and my LinkedIn.",
|
||||
"github": "CrisHenriques",
|
||||
"linkedin": "https://www.linkedin.com/in/crishenriques",
|
||||
"image": "https://i.imgur.com/T6iTQHs.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Viktor Bakurin",
|
||||
"quote": "During my work on freeCodeCamp's nonprofit projects, I found a MEAN stack position outside of my home country. Now I work in Budapest.",
|
||||
"github": "letalumil",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAq6G3QBA1DIwFZGrS4DGqDzBDTzFjrbNQo",
|
||||
"image": "https://i.imgur.com/fvUAWlx.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Ashley Drake",
|
||||
"quote": "freeCodeCamp helped me get my first engineering job. This amazing community made my career switch a lot easier and more fun.",
|
||||
"github": "aldraco",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAABcdBycB1iVHvcW7N3yVK-18ES7Nrxx2jbE",
|
||||
"image": "https://i.imgur.com/xzDoJef.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Brian Grant",
|
||||
"quote": "freeCodeCamp's a great way for disabled veterans like me to retrain. I'm receiving engineering job offers, and I haven't even finished yet.",
|
||||
"github": "codeseekingman",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAEUqXoBFOq1SWQrBsTMHG4ij9Ss4Qqnrtg",
|
||||
"image": "https://i.imgur.com/QPpjPac.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Thomas Joseph Izen",
|
||||
"quote": "After spending lots of time trying different sites that aim to teach people how to code, freeCodeCamp has been different from the beginning. They provide the best and most organized track for anyone at any level to learn how to code and build an amazing, marketable portfolio.",
|
||||
"github": "TommyIzen",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAA79qIYB_RHmvOP59S6VPK3Lm06oG8fM6dw",
|
||||
"image": "https://i.imgur.com/RkO2ISf.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Lori Becker",
|
||||
"quote": "After graduating with a Masters degree in computer science, I could not share any of my code with employers (university policy: fear of aiding cheating). With freeCodeCamp, I was able to develop a small portfolio.",
|
||||
"github": "LCBecker",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAADrtuwBOA_0ihkKIbUFXoXskXikQT9uVeo",
|
||||
"image": "https://i.imgur.com/J1cbqDQ.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Robert Trammel",
|
||||
"quote": "I'm working a job integrating JavaScript into FileMaker and doing some custom web publishing for a school district. I've also have job offers from Apple and a few technology firms around the country. If it wasn't for freeCodeCamp, I'd still be in the dark with some really awesome but irrelevant skills.",
|
||||
"github": "comajama",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAIAABQsM30BEAI6xyAhk-OqbBNUJL0WD2uA3GE",
|
||||
"image": "https://i.imgur.com/E9YdQIn.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Brian Atkins",
|
||||
"quote": "I'm spending less time then I used to on freeCodeCamp, because people have begun to hire me to work on their projects. freeCodeCamp has provided me the foundation I have needed to get these jobs.",
|
||||
"github": "BrianAtk",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAABtTx0BmDzmB7eDGOkAJbRw8RZdvysreso",
|
||||
"image": "https://i.imgur.com/veN77Iw.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Andrea Goulet",
|
||||
"quote": "Before enrolling in freeCodeCamp, I was nervous when people asked me, \"Do you code?\", Now, I answer with a confident \"YES!\"",
|
||||
"github": "andreagoulet",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAA4KWYB-mCMwEU3LvDHXt6H0rVHbBvszq0",
|
||||
"image": "https://i.imgur.com/XWt7fXk.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "David McGill",
|
||||
"quote": "Even if you have a CS degree like I do, you still need projects to prove to employers that you care about coding. freeCodeCamp provided me with a platform for doing this. It's all laid out for you - you just have step up and step into the map!",
|
||||
"github": "dmcgill50",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAEBy74BHsJCpgrbohr2dJxbdXuvMuJDx6k",
|
||||
"image": "https://i.imgur.com/ZVNPIYU.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Adam Recvlohe",
|
||||
"quote": "I had no previous development experience and I was working a full-time as an Instructional Designer. freeCodeCamp helped me get hired as a JavaScript developer.",
|
||||
"github": "arecvlohe",
|
||||
"linkedin": "https://www.linkedin.com/in/adamrecvlohe",
|
||||
"image": "https://i.imgur.com/oFxOlRG.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "John Ellis",
|
||||
"quote": "freeCodeCamp has been one of the major contributors to my career in software development. I started as an apps analyst, spent 3 months going through the coursework in my off time and weekends, and just landed a job as a business systems developer.",
|
||||
"github": "johnmellis",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAbrISEBqAVYOBfhni9mB3YoFFzzrAbYvvo",
|
||||
"image": "https://i.imgur.com/qLfaM9Y.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Rachel Krantz",
|
||||
"quote": "freeCodeCamp gave me confidence that I could apply to software engineering jobs, because I knew I was capable of learning. That confidence convinced the hiring manager to hire me.",
|
||||
"github": "krantzinator",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAOdv-oBnOGtQCoGooUB0xGJ4TWoAgV_z_E",
|
||||
"image": "https://i.imgur.com/dqHrkkC.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Alex Dixon",
|
||||
"quote": "I was hired by the owner of the company, a programmer with 25 years of experience in web development. I graduated from university with a degree in English and had no programming experience prior to attending freeCodeCamp.",
|
||||
"github": "alex-dixon",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAABhCmLMBLAR1AXskaJXDMT-uLPZ8M7TynPQ",
|
||||
"image": "https://i.imgur.com/poiaanK.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Ryan Lindeman",
|
||||
"quote": "freeCodeCamp has been great in giving me a direct path to the full stack development skills I wanted to become more involved in projects at work.",
|
||||
"github": "fai1whale",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAkMT1EBFCY849rMWYSDIEj6kosBJSH9n2s",
|
||||
"image": "https://i.imgur.com/jHOjcyN.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Stephanie Brown",
|
||||
"quote": "freeCodeCamp has given me structure and a direction while learning to code.",
|
||||
"github": "strawmitch",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAARqP5cBDDdBxPUzluctvjUhOP3UsiowRtM",
|
||||
"image": "https://i.imgur.com/k5EEyNf.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Jimson Sulit",
|
||||
"quote": "Aside from the fact that I’m learning full stack web development, freeCodeCamp has also given me the opportunity to lead local community projects.",
|
||||
"github": "webdevjedi25",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAWqUccBopX2Wo_P1gYgy0iIEqChwXPTh2k",
|
||||
"image": "https://i.imgur.com/DzMDPS5.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Brian Emory",
|
||||
"quote": "I like to learn by doing but with the books, while very informative, there was more reading than doing. I came across freeCodeCamp which has allowed me to learn by doing. Whereas before I was struggling to learn, I am now not only learning but learning while building cool projects.",
|
||||
"github": "thebrianemory",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAABc3jXwB-iZdZKZIVAvL93RHGB7_J9gDbVA",
|
||||
"image": "https://i.imgur.com/JcdSD9H.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Ralph Cachero",
|
||||
"quote": "I am a software QA engineer. freeCodeCamp has helped me understand what the developers go through.",
|
||||
"github": "rcachero",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAJEF88BiUtMoxS3Ww7ooI9QmTZdrgP272Q",
|
||||
"image": "https://i.imgur.com/5umfPRq.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Ina Tsetsova",
|
||||
"quote": "Bonfires really make me think and combine programming concepts creatively.",
|
||||
"github": "Tsetsova",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAjPoIcBJsAF91dqwgxFQ4qct88yFcljXNU",
|
||||
"image": "https://i.imgur.com/9Ta15Ow.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Ryan Jones",
|
||||
"quote": "Learning to code with freeCodeCamp has given me a leg up in my career. It has helped to train my brain to think with the logic that computers use. This is a tremendous aid in the field of digital forensics.",
|
||||
"github": "ryanmjones",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAABPJt1MBxC4Yero3PJPhF9rrr_Y7WfOGcCU",
|
||||
"image": "https://i.imgur.com/8cRU20S.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Sara Powell",
|
||||
"quote": "I’ve progressed from not coding very much at all, to coding well enough to land job interviews for front end development positions.",
|
||||
"github": "newtcobell",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAy1jmQBRjoGSUWd6Zib7FtekpSMBVHr7Vw",
|
||||
"image": "https://i.imgur.com/4l4OBbR.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Rhonadale Florentino",
|
||||
"quote": "I can now confidently tell clients that I can design their website.",
|
||||
"github": "None Given",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAI53oUBmy6MPKp1UeHxBy3_y0cyTS4bWow",
|
||||
"image": "https://i.imgur.com/soEDnv6.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Justin Clay Lane",
|
||||
"quote": "freeCodeCamp provided a structured learning experience more akin to an actual class, compared to other free learning sites. I was recently hired to update and maintain the website for a local doctor’s office. The extra money and experience from that is wonderful.",
|
||||
"github": "jclane",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAYZYQ4BBY337OqRUhMnZqDJNX1wNXjT7Bk",
|
||||
"image": "https://i.imgur.com/COEPda5.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Angshuman Gupta",
|
||||
"quote": "I’m a co-founder of a startup. We had been coding with PHP, but always wanted to shift to meteor.js. freeCodeCamp gave me a structured JavaScript guide.",
|
||||
"github": "codingang",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAhIipMB6vAXaratEs0MtUd3GgyYm70cvbE",
|
||||
"image": "https://i.imgur.com/7pwkFQ5.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Genavieve Clausen",
|
||||
"quote": "freeCodeCamp has benefitted me in numerous ways, including the opportunity to learn in a self-paced, supportive environment. I am excited to be a part of a growing community making lasting connections for the career and lifestyle I am pursuing.",
|
||||
"github": "GenavieveMarie",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAOISlMBAi43m1SG-xM_S2B8Vy05yiQz5rE",
|
||||
"image": "https://i.imgur.com/vr7lTkx.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Tim Stauffer",
|
||||
"quote": "I found freeCodeCamp more helpful than my MS degree, so I quit college. Learning so much. Also saving $50,000.",
|
||||
"github": "timstauffer",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAADVcVIBz8UCNjQKl2GUy9ka8UGnQXAXAYw",
|
||||
"image": "https://i.imgur.com/b8YCzf1.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Marquina M Iliev-Piselli",
|
||||
"quote": "I’m re-designing a site for my full-time job.",
|
||||
"github": "Marquina",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAACYvVUBTuu8dNRHthN1TFiyk137PLDqnv4",
|
||||
"image": "https://i.imgur.com/6Ep9hfs.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Pete Considine",
|
||||
"quote": "The guided and structured lessons have been really helpful, as has the relatively slow pace that new concepts are introduced. I had been taking a Udemy course and it really seemed to be skimming the surface of JavaScript in the interest of \"getting to everything.\"",
|
||||
"github": "Pjconsidine",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAAi8-8BxUVpoi_VuJQmeGWN5zhMBgbvPbs",
|
||||
"image": "https://i.imgur.com/SkYRcDW.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Khatra Ahmed",
|
||||
"quote": "I can learn to code with support by my side. Everyone is so helpful and it makes learning to code less of a struggle.",
|
||||
"github": "Mystfreak",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAbdUsIBApacWEmL7CIxe2q7aevMn7aQvmQ",
|
||||
"image": "https://i.imgur.com/XguPEF7.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Marcus Lyons",
|
||||
"quote": "freeCodeCamp has helped me gain the confidence to automate part of my work responsibilities. I was able to use skills I learned from freeCodeCamp to help with writing a bash script to search through mobile app database log files to find errors.",
|
||||
"github": "auron1223",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAA_1aLABo1pVJH9ijSqz8PvLgpzVYkIsjVc",
|
||||
"image": "https://i.imgur.com/X5c77Ov.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Reynald Emmanuel Endaya",
|
||||
"quote": "There is an active community (even in Manila) and it’s interactive, unlike all the MOOCs I tried before where I had to listen to somebody speak for a long time. I am learning a lot here and I have not yet lost my momentum.",
|
||||
"github": "None Given",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAUlm8oBQuXm_Y89_LDC9mb2vOjjQH_pZDo",
|
||||
"image": "https://i.imgur.com/oA5CtWF.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Victoria Kariolic",
|
||||
"quote": "I discovered a coding partner through the freeCodeCamp groups who has been able to cover gaps for my client work.",
|
||||
"github": "Niaskywalk",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAEFmXMBlTFIR2j1G-vJhAMsUOPONILGrLM",
|
||||
"image": "https://i.imgur.com/TiqbM1f.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Cameron Eshgh",
|
||||
"quote": "freeCodeCamp enables me as a digital marketer to dive right into whatever asset or content and fix things that haven’t been working, as well as roll up my sleeves when our web developers are not available.",
|
||||
"github": "eshghitude",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAVdU1MBFFiei4ZYNImnVDcR3H_EiuS6qLY",
|
||||
"image": "https://i.imgur.com/PEzJLCp.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Devarsh Ruparelia",
|
||||
"quote": "Even though I am still just a high school student. The startup I intern for said that if I finish the full track of freeCodeCamp, they will strongly consider me for their programming jobs. Thanks freeCodeCamp!",
|
||||
"github": "devarsh1997",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAApxpP8BZBcHQzr6Ci3xmkkZX-OSH_oLuJs",
|
||||
"image": "https://i.imgur.com/ouv1qUd.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Miranda Bashore",
|
||||
"quote": "I want to be able to freelance and create dynamic websites. freeCodeCamp makes that more of a reality for me, as I cannot afford an expensive bootcamp while getting my Master’s degree.",
|
||||
"github": "DutchBay",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAA8KmJMBTtvvgJzjeAUo_YOssh2yLZZlvlk",
|
||||
"image": "https://i.imgur.com/mJlUzOq.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Christian Morera",
|
||||
"quote": "freeCodeCamp has been a great experience. I’ve learned so many things. I am in the process of transitioning from content developer to full stack developer.",
|
||||
"github": "chrmorfeus",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAMc-tYBtYri0H1KHz1WNQjWxZ23jg0tMNU",
|
||||
"image": "https://i.imgur.com/sfhBDHw.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Jason Arnold",
|
||||
"quote": "I like self-paced learning, so freeCodeCamp has been great. The challenges are difficult enough to push boundaries but not so tough to scare people off.",
|
||||
"github": "thejasonfile",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAABlImsBwAEZ3u3A5NIlCegho8WZ2j4h0w0",
|
||||
"image": "https://i.imgur.com/hqQ3nPA.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Kaveet Laxmidas",
|
||||
"quote": "freeCodeCamp is inspiring me to overhaul some of my old open source projects using more modern approaches and technologies.",
|
||||
"github": "kaveet",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAABZooOgBZJg_0MAJ09pd5vROk83oBFA1cEE",
|
||||
"image": "https://i.imgur.com/1PadpKm.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Brett Guillory",
|
||||
"quote": "freeCodeCamp has given me a great, goal oriented curriculum to learn exactly what I was looking for. And best of all it’s 100% free!",
|
||||
"github": "Kurzninja",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAUMPqMBa6GwMTkA_oHUeqnZzyD95FisjwM",
|
||||
"image": "https://i.imgur.com/WjHNwIu.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Kory J. Campbell",
|
||||
"quote": "I just graduated university, so my financial status is pretty meager, however this camp has definitely helped me improve my skills.",
|
||||
"github": "@koryjcampbell [sic]",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAtxvTgB0N_uJhW-87Dew4wHyeqLUP-XyZk",
|
||||
"image": "https://i.imgur.com/buAYlTA.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Bryon Christopher Miller",
|
||||
"quote": "freeCodeCamp has given me a free, online opportunity to study full stack JavaScript in a structured, community-based format. I am very grateful for this opportunity.",
|
||||
"github": "bryonmiller",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAABEXhHoBxj3Uiq7I0a5v1pVkeJ1gWycbm90",
|
||||
"image": "https://i.imgur.com/ssKAX9d.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Darren Joy",
|
||||
"quote": "Great learning opportunity. Good coding challenges and I’m meeting some very motivated people.",
|
||||
"github": "Darrenfj",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAARv6UIBWeXw4ZfCJ70kBKgnhcv8XgnVsa8",
|
||||
"image": "https://i.imgur.com/sCkeJ0z.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Stephen Mayeux",
|
||||
"quote": "freeCodeCamp is helpful because it’s not 100% hand-holding, and it pushes me out of my comfort zone.",
|
||||
"github": "stephenmayeux",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAjTe7cBhjovoz6zTE_M6MwZ_rr3szhiSOM",
|
||||
"image": "https://i.imgur.com/TbuwAJ3.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "John Hillegass",
|
||||
"quote": "freeCodeCamp has given me the confidence that I need when heading into high level client meetings and developer scrum sessions.",
|
||||
"github": "Ohillio",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAWEO3AB51y8eR2tYF8nydQb8kANkdPwR5U",
|
||||
"image": "https://i.imgur.com/P61e3dt.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Eric Hartline",
|
||||
"quote": "The community is very helpful, and I have already accomplished so much more than what I did with other self-guided courses.",
|
||||
"github": "wildlifehexagon",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAFitUwByB_tgxdExntMnakgQnTK1H3eEd8",
|
||||
"image": "https://i.imgur.com/PgvxbY2.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Danielle J Moss",
|
||||
"quote": "Aside from learning to code in a fun way, I also know I’m not alone and have somewhere to go when I do get stuck and need help.",
|
||||
"github": "psykobilliethekid",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAV4ccABlbMXZ5VfzvlYentPOIKzFbjgbZM",
|
||||
"image": "https://i.imgur.com/AhaRZ3I.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Orcun Tonyali",
|
||||
"quote": "The thorough curriculum helped a lot in managing my company’s website.",
|
||||
"github": "orcuntonyali",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAVwBQIBvE3-M8pDWxzep9umHDnV6JjKmTU",
|
||||
"image": "https://i.imgur.com/54O0p69.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Brendan Murphy",
|
||||
"quote": "I like that it isn’t just a 9 week course. The chat room and partner coding were also very helpful.",
|
||||
"github": "dendari",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAA06V8BqPNnPod-FGRuvifILht-QwZX3YY",
|
||||
"image": "https://i.imgur.com/36Ba4HU.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Michael Berry",
|
||||
"quote": "freeCodeCamp has helped me learn JavaScript, jQuery, and Bootstrap as well as helped me brush up on my HTML and CSS skills. I was laid off from Boeing in April. I’m hoping to land a job as a JavaScript developer as I get closer to finishing the curriculum. I wish I had known about freeCodeCamp sooner.",
|
||||
"github": "Karnblack ",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAGRqf4BwCI3cdJw9wAPc6NlReG3fzOIQq0",
|
||||
"image": "https://i.imgur.com/rIhq3gl.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Angie Canon",
|
||||
"quote": "freeCodeCamp is helping my career. I work with developers and I’m beginning to understand their world better. I can phrase questions more clearly, and begin to guess what an issue might be related to.",
|
||||
"github": "angiecan",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAACfWbABAMsll9ovljvvsLpH317o47hNHX0",
|
||||
"image": "https://i.imgur.com/v258ssn.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Irfan Kucuk",
|
||||
"quote": "I’ve long been looking for a place that could keep me interested in learning how to code. I’ve tried several Codecademy and comparable places, but none have proven as engaging as freeCodeCamp.",
|
||||
"github": "Ikucuk",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAA5ripkBiFAjXkB3ndO6sKiiq6gD21mk6bw",
|
||||
"image": "https://i.imgur.com/Ox8ycSi.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Jonathan Kvicky",
|
||||
"quote": "freeCodeCamp has given me a strong foundational advantage into pursuing a career in software/web development, and has also served as a great platform for connecting with others who share the same passion for software engineering.",
|
||||
"github": "jonkvix",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAABk3i9YB_hcw1AyVg2QHaf8KMQ8ADQ_R_vg",
|
||||
"image": "https://i.imgur.com/CZsAqDK.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Susannah Skyer Gupta",
|
||||
"quote": "As a jack-of-all-trades at a small educational software company, I’m already putting my coding skills to work, beginning to submit my own pull requests for bug fixes instead of just opening a problem ticket and waiting for someone else to fix it.",
|
||||
"github": "SuzGupta",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAFIosEBLewkmbuudMwAqiTM5YE3wHvcE4A",
|
||||
"image": "https://i.imgur.com/sazGykY.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Puneet Shivanand",
|
||||
"quote": "freeCodeCamp has helped me create and maintain a website for a local bioinformatics conference.",
|
||||
"github": "puneet-shivanand",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAArBgXABw4qoSJQoGjqrvU6_vBX1gUmcwGg",
|
||||
"image": "https://i.imgur.com/5BJDA16.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Ian Seabrook",
|
||||
"quote": "I’ve made connections with people I never would have approached before freeCodeCamp.",
|
||||
"github": "ianseabrook",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAuVUi4BSJ3K6fcyTa2fnZr_9Oosb3nKM34",
|
||||
"image": "https://i.imgur.com/z17zai3.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Oleh Kuchuk",
|
||||
"quote": "The coding exercises helped me to prepare for my first job interview.",
|
||||
"github": "drkraken",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAABgy6MwBva2OKpsffAU-OBBeTC7qahTYpGw",
|
||||
"image": "https://i.imgur.com/4KHGgjR.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Larisa Bekerman",
|
||||
"quote": "freeCodeCamp has made me more comfortable with certain aspects of code and helped me review and understand concepts in a more hands-on style. Some people learn by reading theory, I don’t understand things until I’ve made them work myself!",
|
||||
"github": "xaosqueen",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAB-SHUBga96BB-iaHe7QFI-S2dFmeUQaq0",
|
||||
"image": "https://i.imgur.com/EptT3Yl.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Jesse Mull",
|
||||
"quote": "I learned more using freeCodeCamp in three months than I ever could have at a traditional university.",
|
||||
"github": "jessemull",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAr2HfcBswHtmQeLM1rxDEg7GdCuxvNin5s",
|
||||
"image": "https://i.imgur.com/853QDiC.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Mihai Popescu",
|
||||
"quote": "The exercises helped me hone my skills and get an internship at a tech startup. My projects were a great talking point in the interview, showing them what I did and how I approached the problems.",
|
||||
"github": "Mihaisavezi",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAApQxGQBYz-WAQu_0zXPTkW-R7QbdaXEZeA",
|
||||
"image": "https://i.imgur.com/FfrhMjg.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Normandy Real",
|
||||
"quote": "I'm doing freeCodeCamp to transition from mainframe programming to front end development.",
|
||||
"github": "mandyreal",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAdebIIBHfcSRnxGI-j6g5y6crfOXSg55Dc",
|
||||
"image": "https://i.imgur.com/U5jVHGk.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Kristin Anthony",
|
||||
"quote": "The layout, pacing, and resources of freeCodeCamp have given me focus and shown me a path to mastery. Just being able to tell people in my field that I’m learning full stack JavaScript and having projects to show for it has been immensely helpful.",
|
||||
"github": "anthkris",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAZRM5MBACvQe36s4cvpe5ZHWEfgxprDUFg",
|
||||
"image": "https://i.imgur.com/LAE3xBr.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Zlatko Cabric",
|
||||
"quote": "I am still in school, pursuing an AS in web development. The JavaScript course in college was a breeze thanks to freeCodeCamp.",
|
||||
"github": "zlajac ",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAsCCFMBUlE24Ie41G_YS3XhdtQMDl5vCZA",
|
||||
"image": "https://i.imgur.com/X2ozvfP.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Geoff Storbeck",
|
||||
"quote": "I've been able to build tools that has helped moved my current career further and has opened up many more doors in the field.",
|
||||
"github": "storbeck",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAdVQZYBHPYLt5efKwqZmiDM5SqIdH0_AR4",
|
||||
"image": "https://i.imgur.com/DULhJ8z.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Jason Rueckert",
|
||||
"quote": "freeCodeCamp helped me get to the point as a programmer that I felt confident enough to apply for tech jobs. I got the third one I interviewed for.",
|
||||
"github": "jsonify",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAF-rVIBwdzAFzGkWCaIw81O_jajBs-zKZU",
|
||||
"image": "https://i.imgur.com/vSkODHr.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Anthony DePaolo",
|
||||
"quote": "I can learn web development at my own pace, which is great with my crazy life’s schedule. It also doesn’t cost $10k-$17k, which I just can’t fork over right now.",
|
||||
"github": "adepaolo",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAOCUkABzCvw3p1mBiPbbFWq91BEFXGXKxA",
|
||||
"image": "https://i.imgur.com/Dm02s0W.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Jimmy Epperson",
|
||||
"quote": "I learned to build websites, which is now a new service I offer to local businesses.",
|
||||
"github": "jimmyepp",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAADg3N8BTBm_D58gu8Tgq6UPa3OQ_19CrSE",
|
||||
"image": "https://media.licdn.com/mpr/mpr/shrinknp_400_400/AAEAAQAAAAAAAALWAAAAJDUwZDc5YzYwLTc2MjYtNDIzYy1iYzAyLWNlNzZmMTNjM2M1NA.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Jonathan Lucas",
|
||||
"quote": "freeCodeCamp has been nothing but supportive and helpful, taking me from a rather basic knowledge of front end technologies to a better, more complete understanding from a full stack perspective.",
|
||||
"github": "jonslucas",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAATn9H4BI7BP7MBpZ0NR1EvldkWTvAdGy2w",
|
||||
"image": "https://i.imgur.com/Fn91jpJ.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Brian Barrow",
|
||||
"quote": "freeCodeCamp has given me confidence that I can become a web developer.",
|
||||
"github": "briancbarrow",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAVH-osBCI8WZTtv3Om5WjeD2rYnjQ6z7zA",
|
||||
"image": "https://i.imgur.com/VfdBd5Z.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Josh Cronkhite",
|
||||
"quote": "My resume has been bolstered by adding a completely new stack, providing value to my clients and opening up the pool of potential clients that I would have otherwise referred to peers.",
|
||||
"github": "joshcronkhite",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAGTPvoBg__9rivrYrYgo8sDJ561JpAfhHk",
|
||||
"image": "https://i.imgur.com/DjAIqps.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Adam Goswick",
|
||||
"quote": "freeCodeCamp is helping me learn web development when I can’t afford to go back to school.",
|
||||
"github": "thegoz85",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAObbhkBzeCKrzuEB0ssE_iGrBX0Xnu9URc",
|
||||
"image": "https://i.imgur.com/XYdoDCQ.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Travis Wouters",
|
||||
"quote": "freeCodeCamp helped me add skillsets to my resume that show experience instead of knowledge without practical application.",
|
||||
"github": "",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAABHP0YBddxsmaf2OghV2jAy17RVMhig4RM",
|
||||
"image": "https://i.imgur.com/fyRsiOs.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Nicholas Slaven",
|
||||
"quote": "freeCodeCamp has given me the courage to open the door to the idea of programming for a career.",
|
||||
"github": "nslaven22",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAA5HGyIBmpNcXY_tfHBkWxXI6OtwsFAeHRQ",
|
||||
"image": "https://i.imgur.com/9ZYscsm.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "John Bull",
|
||||
"quote": "I am now able to add customized branding and layouts to web applications that fellow employees and customers use everyday. I’m now looking to move away from desktop support and into development roles.",
|
||||
"github": "Jbull328",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAABAyfz8BsjE-oGv1k3URGzhRyeupnTGuK3I",
|
||||
"image": "https://i.imgur.com/U2kJtoX.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "Nick Galluzzo",
|
||||
"quote": "I currently work in a support role for a tech startup. The more I learn about JavaScript, the more I’m able to contribute to a product I really believe in!",
|
||||
"github": "ngalluzzo",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAAjQl1EBZPrbUQ6zGPXmKIuNzpCyqqsnox4",
|
||||
"image": "https://i.imgur.com/edJP2Qt.jpg"
|
||||
},
|
||||
{
|
||||
"camper": "James Allen",
|
||||
"quote": "I finally feel like I can learn to code in my own time and progress to the point of employability.",
|
||||
"github": "None Given",
|
||||
"linkedin": "https://www.linkedin.com/profile/view?id=AAkAAA4OrsIB5WyfuqeECSQO7HYisImVMDiFBl0",
|
||||
"image": "https://i.imgur.com/quRxESK.jpg"
|
||||
}
|
||||
]
|
86
api-server/server/rss/index.js
Normal file
86
api-server/server/rss/index.js
Normal file
@ -0,0 +1,86 @@
|
||||
import _ from 'lodash';
|
||||
import compareDesc from 'date-fns/compare_desc';
|
||||
import debug from 'debug';
|
||||
|
||||
import { getMediumFeed } from './medium';
|
||||
import { getLybsynFeed } from './lybsyn';
|
||||
|
||||
const log = debug('fcc:rss:news-feed');
|
||||
|
||||
const fiveMinutes = 1000 * 60 * 5;
|
||||
|
||||
class NewsFeed {
|
||||
constructor() {
|
||||
|
||||
this.state = {
|
||||
readyState: false,
|
||||
mediumFeed: [],
|
||||
lybsynFeed: [],
|
||||
combinedFeed: []
|
||||
};
|
||||
this.refreshFeeds();
|
||||
|
||||
setInterval(this.refreshFeeds, fiveMinutes);
|
||||
}
|
||||
|
||||
setState = stateUpdater => {
|
||||
const newState = stateUpdater(this.state);
|
||||
this.state = _.merge({}, this.state, newState);
|
||||
return;
|
||||
}
|
||||
|
||||
refreshFeeds = () => {
|
||||
const currentFeed = this.state.combinedFeed.slice(0);
|
||||
log('grabbing feeds');
|
||||
return Promise.all([
|
||||
getMediumFeed(),
|
||||
getLybsynFeed()
|
||||
]).then(
|
||||
([mediumFeed, lybsynFeed]) => this.setState(
|
||||
state => ({
|
||||
...state,
|
||||
mediumFeed,
|
||||
lybsynFeed
|
||||
})
|
||||
))
|
||||
.then(() => {
|
||||
log('crossing the streams');
|
||||
const { mediumFeed, lybsynFeed} = this.state;
|
||||
const combinedFeed = [ ...mediumFeed, ...lybsynFeed ].sort((a, b) => {
|
||||
return compareDesc(a.isoDate, b.isoDate);
|
||||
});
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
combinedFeed,
|
||||
readyState: true
|
||||
}));
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
combinedFeed: currentFeed
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
getFeed = () => new Promise((resolve) => {
|
||||
let notReadyCount = 0;
|
||||
|
||||
function waitForReady() {
|
||||
log('notReadyCount', notReadyCount);
|
||||
notReadyCount++;
|
||||
return this.state.readyState || notReadyCount === 5 ?
|
||||
resolve(this.state.combinedFeed) :
|
||||
setTimeout(waitForReady, 100);
|
||||
}
|
||||
log('are we ready?', this.state.readyState);
|
||||
return this.state.readyState ?
|
||||
resolve(this.state.combinedFeed) :
|
||||
setTimeout(waitForReady, 100);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
export default NewsFeed;
|
47
api-server/server/rss/lybsyn.js
Normal file
47
api-server/server/rss/lybsyn.js
Normal file
@ -0,0 +1,47 @@
|
||||
import http from 'http';
|
||||
import _ from 'lodash';
|
||||
|
||||
const lybsynFeed = 'http://freecodecamp.libsyn.com/render-type/json';
|
||||
|
||||
export function getLybsynFeed() {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(lybsynFeed, res => {
|
||||
let raw = '';
|
||||
|
||||
res.on('data', chunk => {
|
||||
raw += chunk;
|
||||
});
|
||||
|
||||
res.on('error', err => reject(err));
|
||||
|
||||
res.on('end', () => {
|
||||
let feed = [];
|
||||
|
||||
try {
|
||||
feed = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const items = feed.map(
|
||||
item => _.pick(item, [
|
||||
'full_item_url',
|
||||
'item_title',
|
||||
'release_date',
|
||||
'item_body_short'
|
||||
])
|
||||
)
|
||||
/* eslint-disable camelcase */
|
||||
.map(({ full_item_url, item_title, release_date, item_body_short}) => ({
|
||||
title: item_title,
|
||||
extract: item_body_short,
|
||||
isoDate: new Date(release_date).toISOString(),
|
||||
link: full_item_url
|
||||
}));
|
||||
/* eslint-enable camelcase */
|
||||
return resolve(items);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
39
api-server/server/rss/medium.js
Normal file
39
api-server/server/rss/medium.js
Normal file
@ -0,0 +1,39 @@
|
||||
import Parser from 'rss-parser';
|
||||
import _ from 'lodash';
|
||||
|
||||
const parser = new Parser();
|
||||
|
||||
const mediumFeed = 'https://medium.freecodecamp.org/feed';
|
||||
|
||||
function getExtract(str) {
|
||||
return str.slice(0, str.indexOf('</p>') + 4);
|
||||
}
|
||||
|
||||
|
||||
function addResponsiveClass(str) {
|
||||
return str.replace(/\<img/g, '<img class="img-responsive"');
|
||||
}
|
||||
|
||||
export function getMediumFeed() {
|
||||
return new Promise((resolve, reject) => {
|
||||
parser.parseURL(mediumFeed, (err, feed) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
const items = feed.items
|
||||
.map(
|
||||
item => _.pick(item, ['title', 'link', 'isoDate', 'content:encoded'])
|
||||
)
|
||||
.map(
|
||||
(item) => ({
|
||||
...item,
|
||||
extract: getExtract(item['content:encoded'])
|
||||
})
|
||||
)
|
||||
.map(item => _.omit(item, ['content:encoded']))
|
||||
.map(item => ({ ...item, extract: addResponsiveClass(item.extract)}));
|
||||
resolve(items);
|
||||
});
|
||||
});
|
||||
}
|
77
api-server/server/server.js
Executable file
77
api-server/server/server.js
Executable file
@ -0,0 +1,77 @@
|
||||
require('dotenv').load();
|
||||
|
||||
const _ = require('lodash');
|
||||
const Rx = require('rx');
|
||||
const loopback = require('loopback');
|
||||
const boot = require('loopback-boot');
|
||||
const expressState = require('express-state');
|
||||
const path = require('path');
|
||||
const createDebugger = require('debug');
|
||||
|
||||
const { setupPassport } = require('./component-passport');
|
||||
|
||||
const log = createDebugger('fcc:server');
|
||||
// force logger to always output
|
||||
// this may be brittle
|
||||
log.enabled = true;
|
||||
|
||||
Rx.config.longStackSupport = process.env.NODE_DEBUG !== 'production';
|
||||
const app = loopback();
|
||||
const isBeta = !!process.env.BETA;
|
||||
|
||||
expressState.extend(app);
|
||||
app.set('state namespace', '__fcc__');
|
||||
app.set('port', process.env.PORT || 3000);
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
app.use(loopback.token());
|
||||
app.disable('x-powered-by');
|
||||
|
||||
boot(app, {
|
||||
appRootDir: __dirname,
|
||||
dev: process.env.NODE_ENV
|
||||
});
|
||||
|
||||
setupPassport(app);
|
||||
|
||||
const { db } = app.datasources;
|
||||
db.on('connected', _.once(() => log('db connected')));
|
||||
app.start = _.once(function() {
|
||||
const server = app.listen(app.get('port'), function() {
|
||||
app.emit('started');
|
||||
log(
|
||||
'freeCodeCamp server listening on port %d in %s',
|
||||
app.get('port'),
|
||||
app.get('env')
|
||||
);
|
||||
if (isBeta) {
|
||||
log('freeCodeCamp is in beta mode');
|
||||
}
|
||||
log(`connecting to db at ${db.settings.url}`);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
log('Shutting down server');
|
||||
server.close(() => {
|
||||
log('Server is closed');
|
||||
});
|
||||
log('closing db connection');
|
||||
db.disconnect().then(() => {
|
||||
log('DB connection closed');
|
||||
// exit process
|
||||
// this may close kept alive sockets
|
||||
// eslint-disable-next-line no-process-exit
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
// start the server if `$ node server.js`
|
||||
// in production use `$npm start-production`
|
||||
// or `$node server/production` to start the server
|
||||
// and wait for DB handshake
|
||||
if (require.main === module) {
|
||||
app.start();
|
||||
}
|
58
api-server/server/services/challenge.js
Normal file
58
api-server/server/services/challenge.js
Normal file
@ -0,0 +1,58 @@
|
||||
import debug from 'debug';
|
||||
import { pickBy } from 'lodash';
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import { cachedMap, getChallenge } from '../utils/map';
|
||||
import { shapeChallenges } from '../../common/app/redux/utils';
|
||||
|
||||
const log = debug('fcc:services:challenge');
|
||||
const isDev = debug.enabled('fcc:*');
|
||||
|
||||
export default function getChallengesForBlock(app) {
|
||||
const challengeMap = cachedMap(app.models);
|
||||
return {
|
||||
name: 'challenge',
|
||||
read: function readChallengesForBlock(
|
||||
req,
|
||||
resource,
|
||||
{ dashedName, blockName} = {},
|
||||
config,
|
||||
cb
|
||||
) {
|
||||
const getChallengeBlock$ = challengeMap
|
||||
.flatMap(({
|
||||
result: { superBlocks },
|
||||
entities: {
|
||||
block: fullBlockMap,
|
||||
challenge: challengeMap
|
||||
}
|
||||
}) => {
|
||||
log(`sourcing challenges for the ${blockName} block`);
|
||||
const requestedChallenges = pickBy(
|
||||
challengeMap,
|
||||
ch => ch.block === blockName
|
||||
);
|
||||
const entities = {
|
||||
block: {
|
||||
[blockName]: fullBlockMap[blockName]
|
||||
},
|
||||
challenge: requestedChallenges
|
||||
};
|
||||
const { challenge, block } = shapeChallenges(entities, isDev);
|
||||
return Observable.of({
|
||||
result: { superBlocks },
|
||||
entities: { challenge, block }
|
||||
});
|
||||
});
|
||||
return Observable.if(
|
||||
() => !!dashedName,
|
||||
getChallenge(dashedName, blockName, challengeMap, 'en'),
|
||||
getChallengeBlock$
|
||||
)
|
||||
.subscribe(
|
||||
result => cb(null, result),
|
||||
cb
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
86
api-server/server/services/mapUi.js
Normal file
86
api-server/server/services/mapUi.js
Normal file
@ -0,0 +1,86 @@
|
||||
import debug from 'debug';
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import { cachedMap } from '../utils/map';
|
||||
|
||||
const log = debug('fcc:services:mapUi');
|
||||
|
||||
|
||||
export default function mapUiService(app) {
|
||||
const challengeMap = cachedMap(app.models);
|
||||
return {
|
||||
name: 'map-ui',
|
||||
read: function readMapUi(req, resource, _, config, cb) {
|
||||
return challengeMap
|
||||
.flatMap(({
|
||||
result: { superBlocks },
|
||||
entities: {
|
||||
superBlock: fullSuperBlockMap,
|
||||
block: fullBlockMap,
|
||||
challenge: fullChallengeMap
|
||||
}
|
||||
}) => {
|
||||
const superBlockMap = superBlocks
|
||||
.map(superBlock => fullSuperBlockMap[superBlock])
|
||||
.reduce((map, { dashedName, blocks, title }) => {
|
||||
map[dashedName] = { blocks, title, dashedName};
|
||||
return map;
|
||||
}, {});
|
||||
const blockMap = Object.keys(fullBlockMap)
|
||||
.map(block => fullBlockMap[block])
|
||||
.reduce(
|
||||
(map, { dashedName, title, time, challenges, superBlock }) => {
|
||||
map[dashedName] = {
|
||||
dashedName,
|
||||
title,
|
||||
time,
|
||||
challenges,
|
||||
superBlock
|
||||
};
|
||||
return map;
|
||||
},
|
||||
{}
|
||||
);
|
||||
const challengeMap = Object.keys(fullChallengeMap)
|
||||
.map(challenge => fullChallengeMap[challenge])
|
||||
.reduce((map, challenge) => {
|
||||
const {
|
||||
dashedName,
|
||||
id,
|
||||
title,
|
||||
name,
|
||||
block,
|
||||
isLocked,
|
||||
isComingSoon,
|
||||
isBeta,
|
||||
challengeType
|
||||
} = challenge;
|
||||
map[dashedName] = {
|
||||
dashedName,
|
||||
id,
|
||||
title,
|
||||
name,
|
||||
block,
|
||||
isLocked,
|
||||
isComingSoon,
|
||||
isBeta,
|
||||
challengeType
|
||||
};
|
||||
return map;
|
||||
}, {});
|
||||
const mapUi = {
|
||||
result: { superBlocks },
|
||||
entities: {
|
||||
superBlock: superBlockMap,
|
||||
block: blockMap,
|
||||
challenge: challengeMap
|
||||
}
|
||||
};
|
||||
return Observable.of(mapUi);
|
||||
}).subscribe(
|
||||
mapUi => cb(null, mapUi ),
|
||||
err => { log(err); return cb(err); }
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
73
api-server/server/services/user.js
Normal file
73
api-server/server/services/user.js
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
*
|
||||
* Any ref to fixCompletedChallengesItem should be removed post
|
||||
* a db migration to fix all completedChallenges
|
||||
*
|
||||
*/
|
||||
|
||||
import { Observable } from 'rx';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {
|
||||
getProgress,
|
||||
normaliseUserFields,
|
||||
userPropsForSession
|
||||
} from '../utils/publicUserProps';
|
||||
import { fixCompletedChallengeItem } from '../../common/utils';
|
||||
|
||||
export default function userServices() {
|
||||
return {
|
||||
name: 'user',
|
||||
read: function readUserService(
|
||||
req,
|
||||
resource,
|
||||
params,
|
||||
config,
|
||||
cb) {
|
||||
const queryUser = req.user;
|
||||
console.log(queryUser.completedChallengeCount)
|
||||
const source = queryUser && Observable.forkJoin(
|
||||
queryUser.getCompletedChallenges$(),
|
||||
queryUser.getPoints$(),
|
||||
(completedChallenges, progressTimestamps) => ({
|
||||
completedChallenges,
|
||||
progress: getProgress(progressTimestamps, queryUser.timezone)
|
||||
})
|
||||
);
|
||||
Observable.if(
|
||||
() => !queryUser,
|
||||
Observable.of({}),
|
||||
Observable.defer(() => source)
|
||||
.map(({ completedChallenges, progress }) => ({
|
||||
...queryUser.toJSON(),
|
||||
...progress,
|
||||
completedChallenges: completedChallenges.map(
|
||||
fixCompletedChallengeItem
|
||||
)
|
||||
}))
|
||||
.map(
|
||||
user => ({
|
||||
entities: {
|
||||
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 => cb(null, user),
|
||||
cb
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
95
api-server/server/utils/about.js
Normal file
95
api-server/server/utils/about.js
Normal file
@ -0,0 +1,95 @@
|
||||
import _ from 'lodash';
|
||||
import debug from 'debug';
|
||||
import dedent from 'dedent';
|
||||
import fs from 'fs';
|
||||
import goog from 'googleapis';
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import { timeCache, observeMethod } from './rx';
|
||||
|
||||
// one million!
|
||||
const upperBound = 1000 * 1000;
|
||||
const scope = 'https://www.googleapis.com/auth/analytics.readonly';
|
||||
const pathToCred = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
||||
|
||||
const log = debug('fcc:server:utils:about');
|
||||
const analytics = goog.analytics('v3');
|
||||
const makeRequest = observeMethod(analytics.data.realtime, 'get');
|
||||
export const toBoundInt = _.flow(
|
||||
// first convert string to integer
|
||||
_.toInteger,
|
||||
// then we bound the integer to prevent weird things like Infinity
|
||||
// and negative numbers
|
||||
// can't wait to the day we need to update this!
|
||||
_.partialRight(_.clamp, 0, upperBound)
|
||||
);
|
||||
|
||||
export function createActiveUsers() {
|
||||
const zero = Observable.of(0);
|
||||
let credentials;
|
||||
if (!pathToCred) {
|
||||
// if no path to credentials set to zero;
|
||||
log(dedent`
|
||||
no google applications credentials environmental variable found
|
||||
'GOOGLE_APPLICATION_CREDENTIALS'
|
||||
'activeUser' api will always return 0
|
||||
this can safely be ignored during development
|
||||
`);
|
||||
return zero;
|
||||
}
|
||||
try {
|
||||
credentials = require(fs.realpathSync(pathToCred));
|
||||
} catch (err) {
|
||||
log('google applications credentials file failed to require');
|
||||
console.error(err);
|
||||
// if we can't require credentials set to zero;
|
||||
return zero;
|
||||
}
|
||||
if (
|
||||
!credentials.private_key ||
|
||||
!credentials.client_email ||
|
||||
!credentials.viewId
|
||||
) {
|
||||
log(dedent`
|
||||
google applications credentials json should have a
|
||||
* private_key
|
||||
* client_email
|
||||
* viewId
|
||||
but none were found
|
||||
`);
|
||||
return zero;
|
||||
}
|
||||
|
||||
const client = new goog.auth.JWT(
|
||||
credentials['client_email'],
|
||||
null,
|
||||
credentials['private_key'],
|
||||
[scope],
|
||||
);
|
||||
const authorize = observeMethod(client, 'authorize');
|
||||
const options = {
|
||||
ids: `ga:${credentials.viewId}`,
|
||||
auth: client,
|
||||
metrics: 'rt:activeUsers'
|
||||
};
|
||||
return Observable.defer(
|
||||
// we wait for authorize to complete before attempting to make request
|
||||
// this ensures our token is initialized and valid
|
||||
// we defer here to make sure the actual request is done per subscription
|
||||
// instead of once at startup
|
||||
() => authorize().flatMap(() => makeRequest(options))
|
||||
)
|
||||
// data: Array[body|Object, request: Request]
|
||||
.map(data => data[0])
|
||||
.map(
|
||||
({ totalsForAllResults } = {}) => totalsForAllResults['rt:activeUsers']
|
||||
)
|
||||
.map(toBoundInt)
|
||||
// print errors to error log for logging, duh
|
||||
.do(null, err => console.error(err))
|
||||
// always send a number down
|
||||
.catch(() => Observable.of(0))
|
||||
// cache for 2 seconds to prevent hitting our daily request limit
|
||||
::timeCache(2, 'seconds');
|
||||
}
|
||||
|
56
api-server/server/utils/auth.js
Normal file
56
api-server/server/utils/auth.js
Normal file
@ -0,0 +1,56 @@
|
||||
const githubRegex = (/github/i);
|
||||
const providerHash = {
|
||||
facebook: ({ id }) => id,
|
||||
github: ({ username }) => username,
|
||||
twitter: ({ username }) => username,
|
||||
linkedin({ _json }) {
|
||||
return _json && _json.publicProfileUrl || null;
|
||||
},
|
||||
google: ({ id }) => id
|
||||
};
|
||||
|
||||
export function getUsernameFromProvider(provider, profile) {
|
||||
return typeof providerHash[provider] === 'function' ?
|
||||
providerHash[provider](profile) :
|
||||
null;
|
||||
}
|
||||
|
||||
// createProfileAttributes(provider: String, profile: {}) => Object
|
||||
export function createUserUpdatesFromProfile(provider, profile) {
|
||||
if (githubRegex.test(provider)) {
|
||||
return createProfileAttributesFromGithub(profile);
|
||||
}
|
||||
return {
|
||||
[getSocialProvider(provider)]: getUsernameFromProvider(
|
||||
getSocialProvider(provider),
|
||||
profile
|
||||
)
|
||||
};
|
||||
}
|
||||
// createProfileAttributes(profile) => profileUpdate
|
||||
function createProfileAttributesFromGithub(profile) {
|
||||
const {
|
||||
profileUrl: githubProfile,
|
||||
username,
|
||||
_json: {
|
||||
avatar_url: picture,
|
||||
blog: website,
|
||||
location,
|
||||
bio,
|
||||
name
|
||||
} = {}
|
||||
} = profile;
|
||||
return {
|
||||
name,
|
||||
username: username.toLowerCase(),
|
||||
location,
|
||||
bio,
|
||||
website,
|
||||
picture,
|
||||
githubProfile
|
||||
};
|
||||
}
|
||||
|
||||
export function getSocialProvider(provider) {
|
||||
return provider.split('-')[0];
|
||||
}
|
19
api-server/server/utils/bad-id-map.js
Normal file
19
api-server/server/utils/bad-id-map.js
Normal file
@ -0,0 +1,19 @@
|
||||
export default {
|
||||
bg9997c9c79feddfaeb9bdef: '56bbb991ad1ed5201cd392ca',
|
||||
bg9995c9c69feddfaeb9bdef: '56bbb991ad1ed5201cd392cb',
|
||||
bg9994c9c69feddfaeb9bdef: '56bbb991ad1ed5201cd392cc',
|
||||
bg9996c9c69feddfaeb9bdef: '56bbb991ad1ed5201cd392cd',
|
||||
bg9997c9c69feddfaeb9bdef: '56bbb991ad1ed5201cd392ce',
|
||||
bg9997c9c89feddfaeb9bdef: '56bbb991ad1ed5201cd392cf',
|
||||
bg9998c9c99feddfaeb9bdef: '56bbb991ad1ed5201cd392d0',
|
||||
bg9999c9c99feddfaeb9bdef: '56bbb991ad1ed5201cd392d1',
|
||||
bg9999c9c99feedfaeb9bdef: '56bbb991ad1ed5201cd392d2',
|
||||
bg9999c9c99fdddfaeb9bdef: '56bbb991ad1ed5201cd392d3',
|
||||
bb000000000000000000001: '56bbb991ad1ed5201cd392d4',
|
||||
bc000000000000000000001: '56bbb991ad1ed5201cd392d5',
|
||||
bb000000000000000000002: '56bbb991ad1ed5201cd392d6',
|
||||
bb000000000000000000003: '56bbb991ad1ed5201cd392d7',
|
||||
bb000000000000000000004: '56bbb991ad1ed5201cd392d8',
|
||||
bb000000000000000000005: '56bbb991ad1ed5201cd392d9',
|
||||
bb000000000000000000006: '56bbb991ad1ed5201cd392da'
|
||||
};
|
12
api-server/server/utils/certTypes.json
Normal file
12
api-server/server/utils/certTypes.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"frontEnd": "isFrontEndCert",
|
||||
"backEnd": "isBackEndCert",
|
||||
"dataVis": "isDataVisCert",
|
||||
"respWebDesign": "isRespWebDesignCert",
|
||||
"frontEndLibs": "isFrontEndLibsCert",
|
||||
"dataVis2018": "is2018DataVisCert",
|
||||
"jsAlgoDataStruct": "isJsAlgoDataStructCert",
|
||||
"apisMicroservices": "isApisMicroservicesCert",
|
||||
"infosecQa": "isInfosecQaCert",
|
||||
"fullStack": "isFullStackCert"
|
||||
}
|
11
api-server/server/utils/commit-goals.json
Normal file
11
api-server/server/utils/commit-goals.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"frontEndCert": "Front End Development Certification",
|
||||
"backEndCert": "Back End Development Certification",
|
||||
"fullStackCert": "Full Stack Development Certification",
|
||||
"respWebDesign": "Responsive Web Design Certification",
|
||||
"frontEndLibs": "Front End Libraries Certification",
|
||||
"jsAlgoDataStruct": "JavaScript Algorithms and Data Structures Certification",
|
||||
"dataVis": "Data Visualisation Certification",
|
||||
"apisMicroservices": "APIs and Microservices Certification",
|
||||
"infosecQa": "Information Security and Quality Assurance Certification"
|
||||
}
|
55
api-server/server/utils/commit.js
Normal file
55
api-server/server/utils/commit.js
Normal file
@ -0,0 +1,55 @@
|
||||
import dedent from 'dedent';
|
||||
import debugFactory from 'debug';
|
||||
import { Observable } from 'rx';
|
||||
import commitGoals from './commit-goals.json';
|
||||
|
||||
const debug = debugFactory('fcc:utils/commit');
|
||||
|
||||
export { commitGoals };
|
||||
|
||||
export function completeCommitment$(user) {
|
||||
const {
|
||||
isFrontEndCert,
|
||||
isBackEndCert,
|
||||
isFullStackCert,
|
||||
isRespWebDesignCert,
|
||||
isFrontEndLibsCert,
|
||||
isJsAlgoDataStructCert,
|
||||
isDataVisCert,
|
||||
isApisMicroservicesCert,
|
||||
isInfosecQaCert
|
||||
} = user;
|
||||
|
||||
return Observable.fromNodeCallback(user.pledge, user)()
|
||||
.flatMap(pledge => {
|
||||
if (!pledge) {
|
||||
return Observable.just('No pledge found');
|
||||
}
|
||||
|
||||
const { goal } = pledge;
|
||||
|
||||
if (
|
||||
(isFrontEndCert && goal === commitGoals.frontEndCert) ||
|
||||
(isBackEndCert && goal === commitGoals.backEndCert) ||
|
||||
(isFullStackCert && goal === commitGoals.fullStackCert) ||
|
||||
(isRespWebDesignCert && goal === commitGoals.respWebDesignCert) ||
|
||||
(isFrontEndLibsCert && goal === commitGoals.frontEndLibsCert) ||
|
||||
(isJsAlgoDataStructCert && goal === commitGoals.jsAlgoDataStructCert) ||
|
||||
(isDataVisCert && goal === commitGoals.dataVisCert) ||
|
||||
(isApisMicroservicesCert &&
|
||||
goal === commitGoals.apisMicroservicesCert) ||
|
||||
(isInfosecQaCert && goal === commitGoals.infosecQaCert)
|
||||
) {
|
||||
debug('marking goal complete');
|
||||
pledge.isCompleted = true;
|
||||
pledge.dateEnded = new Date();
|
||||
pledge.formerUserId = pledge.userId;
|
||||
pledge.userId = null;
|
||||
return Observable.fromNodeCallback(pledge.save, pledge)();
|
||||
}
|
||||
return Observable.just(dedent`
|
||||
You have not yet reached your goal of completing the ${goal}
|
||||
Please retry when you have met the requirements.
|
||||
`);
|
||||
});
|
||||
}
|
50
api-server/server/utils/commit.json
Normal file
50
api-server/server/utils/commit.json
Normal file
@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"name": "girl develop it",
|
||||
"displayName": "Girl Develop It",
|
||||
"donateUrl": "https://www.girldevelopit.com/donate",
|
||||
"description": "Girl Develop It provides in-person classes for women to learn to code.",
|
||||
"imgAlt": "Girl Develop It participants coding at tables.",
|
||||
"imgUrl": "https://i.imgur.com/U1CyEuA.jpg"
|
||||
},
|
||||
{
|
||||
"name": "black girls code",
|
||||
"displayName": "Black Girls CODE",
|
||||
"donateUrl": "http://www.blackgirlscode.com/",
|
||||
"description": "Black Girls CODE is devoted to showing the world that black girls can code, and do so much more.",
|
||||
"imgAlt": "Girls developing code with instructor",
|
||||
"imgUrl": "https://i.imgur.com/HBVrdaj.jpg"
|
||||
},
|
||||
{
|
||||
"name": "coderdojo",
|
||||
"displayName": "CoderDojo",
|
||||
"donateUrl": "https://www.globalgiving.org/projects/coderdojo-start-a-dojo-support/",
|
||||
"description": "CoderDojo is the global network of free computer programming clubs for young people.",
|
||||
"imgAlt": "Two adults help several kids program on their laptops.",
|
||||
"imgUrl": "https://i.imgur.com/701RLfV.jpg"
|
||||
},
|
||||
{
|
||||
"name": "women who code",
|
||||
"displayName": "Women Who Code",
|
||||
"donateUrl": "https://www.womenwhocode.com/donate",
|
||||
"description": "Women Who Code (WWCode) is a global leader in propelling women in the tech industry.",
|
||||
"imgAlt": "Four women sitting in a classroom together learning to code.",
|
||||
"imgUrl": "https://i.imgur.com/tKUi6Rf.jpg"
|
||||
},
|
||||
{
|
||||
"name": "girls who code",
|
||||
"displayName": "Girls Who Code",
|
||||
"donateUrl": "http://girlswhocode.com/",
|
||||
"description": "Girls Who Code programs work to inspire, educate, and equip girls with the computing skills to pursue 21st century opportunities.",
|
||||
"imgAlt": "Three women smiling while they code on a computer together.",
|
||||
"imgUrl": "https://i.imgur.com/op8BVph.jpg"
|
||||
},
|
||||
{
|
||||
"name": "hack club",
|
||||
"displayName": "Hack Club",
|
||||
"donateUrl": "https://hackclub.com/donate",
|
||||
"description": "Hack Club helps high schoolers start after-school coding clubs.",
|
||||
"imgAlt": "A bunch of high school students posing for a photo in their Hack Club.",
|
||||
"imgUrl": "https://i.imgur.com/G2YvPHf.jpg"
|
||||
}
|
||||
]
|
15
api-server/server/utils/constantStrings.json
Normal file
15
api-server/server/utils/constantStrings.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"gitHubUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1521.3 Safari/537.36",
|
||||
|
||||
"legacyFrontEndChallengeId": "561add10cb82ac38a17513be",
|
||||
"legacyBackEndChallengeId": "660add10cb82ac38a17513be",
|
||||
"legacyDataVisId": "561add10cb82ac39a17513bc",
|
||||
|
||||
"respWebDesignId": "561add10cb82ac38a17513bc",
|
||||
"frontEndLibsId": "561acd10cb82ac38a17513bc",
|
||||
"dataVis2018Id": "5a553ca864b52e1d8bceea14",
|
||||
"jsAlgoDataStructId": "561abd10cb81ac38a17513bc",
|
||||
"apisMicroservicesId": "561add10cb82ac38a17523bc",
|
||||
"infosecQaId": "561add10cb82ac38a17213bc",
|
||||
"fullStackId": "561add10cb82ac38a17213bd"
|
||||
}
|
75
api-server/server/utils/constants.js
Normal file
75
api-server/server/utils/constants.js
Normal file
@ -0,0 +1,75 @@
|
||||
let alphabet = '';
|
||||
|
||||
for (let i = 0; i < 26; i++) {
|
||||
alphabet = alphabet.concat(String.fromCharCode(97 + i));
|
||||
}
|
||||
|
||||
export const blacklistedUsernames = [
|
||||
...alphabet.split(''),
|
||||
'about',
|
||||
'academic-honesty',
|
||||
'account',
|
||||
'agile',
|
||||
'all-stories',
|
||||
'api',
|
||||
'backend-challenge-completed',
|
||||
'bonfire',
|
||||
'cats.json',
|
||||
'challenge',
|
||||
'challenge-completed',
|
||||
'challenges',
|
||||
'chat',
|
||||
'coding-bootcamp-cost-calculator',
|
||||
'completed-bonfire',
|
||||
'completed-challenge',
|
||||
'completed-field-guide',
|
||||
'completed-zipline-or-basejump',
|
||||
'donate',
|
||||
'events',
|
||||
'explorer',
|
||||
'external',
|
||||
'field-guide',
|
||||
'forgot',
|
||||
'forum',
|
||||
'get-help',
|
||||
'get-help',
|
||||
'get-pai',
|
||||
'guide',
|
||||
'how-nonprofit-projects-work',
|
||||
'internal',
|
||||
'jobs',
|
||||
'jobs-form',
|
||||
'learn-to-code',
|
||||
'login',
|
||||
'logout',
|
||||
'map',
|
||||
'modern-challenge-completed',
|
||||
'news',
|
||||
'nonprofits',
|
||||
'nonproifts-form',
|
||||
'open-api',
|
||||
'pmi-acp-agile-project-managers',
|
||||
'pmi-acp-agile-project-managers-form',
|
||||
'privacy',
|
||||
'privacy',
|
||||
'project-completed',
|
||||
'reset',
|
||||
'services',
|
||||
'signin',
|
||||
'signout',
|
||||
'sitemap.xml',
|
||||
'software-resources-for-nonprofits',
|
||||
'stories',
|
||||
'terms',
|
||||
'the-fastest-web-page-on-the-internet',
|
||||
'twitch',
|
||||
'unsubscribe',
|
||||
'unsubscribed',
|
||||
'update-my-portfolio',
|
||||
'update-my-profile-ui',
|
||||
'update-my-projects',
|
||||
'update-my-theme',
|
||||
'update-my-username',
|
||||
'user',
|
||||
'wiki'
|
||||
];
|
32
api-server/server/utils/create-handled-error.js
Normal file
32
api-server/server/utils/create-handled-error.js
Normal file
@ -0,0 +1,32 @@
|
||||
const _handledError = Symbol('handledError');
|
||||
|
||||
export function isHandledError(err) {
|
||||
return !!err[_handledError];
|
||||
}
|
||||
|
||||
export function unwrapHandledError(err) {
|
||||
return err[_handledError] || {};
|
||||
}
|
||||
|
||||
export function wrapHandledError(err, {
|
||||
type,
|
||||
message,
|
||||
redirectTo,
|
||||
status = 200
|
||||
}) {
|
||||
err[_handledError] = { type, message, redirectTo, status };
|
||||
return err;
|
||||
}
|
||||
|
||||
// for use with express-validator error formatter
|
||||
export const createValidatorErrorFormatter = (type, redirectTo) =>
|
||||
({ msg }) => wrapHandledError(
|
||||
new Error(msg),
|
||||
{
|
||||
type,
|
||||
message: msg,
|
||||
redirectTo,
|
||||
// we default to 400 as these are malformed requests
|
||||
status: 400
|
||||
}
|
||||
);
|
11
api-server/server/utils/date-utils.js
Normal file
11
api-server/server/utils/date-utils.js
Normal file
@ -0,0 +1,11 @@
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
// day count between two epochs (inclusive)
|
||||
export function dayCount([head, tail], timezone = 'UTC') {
|
||||
return Math.ceil(
|
||||
moment(moment(head).tz(timezone).endOf('day')).diff(
|
||||
moment(tail).tz(timezone).startOf('day'),
|
||||
'days',
|
||||
true)
|
||||
);
|
||||
}
|
75
api-server/server/utils/date-utils.test.js
Normal file
75
api-server/server/utils/date-utils.test.js
Normal file
@ -0,0 +1,75 @@
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { dayCount } from './date-utils';
|
||||
import test from 'tape';
|
||||
|
||||
const PST = 'America/Los_Angeles';
|
||||
|
||||
test('Day count between two epochs (inclusive) calculation', function(t) {
|
||||
t.plan(7);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('8/3/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
1,
|
||||
'should return 1 day given epochs of the same day'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
1,
|
||||
'should return 1 day given same epochs'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('8/4/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
2,
|
||||
'should return 2 days when there is a 24 hours difference'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('8/4/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 23:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
2,
|
||||
'should return 2 days when the diff is less than 24h but different in UTC'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('8/4/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 23:00', 'M/D/YYYY H:mm').valueOf()
|
||||
], PST),
|
||||
1,
|
||||
'should return 1 day when the diff is less than 24h ' +
|
||||
'and days are different in UTC, but given PST'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('10/27/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('5/12/1982 1:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
12222,
|
||||
'should return correct count when there is very big period'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('8/4/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
2,
|
||||
'should return 2 days when there is a 24 hours difference ' +
|
||||
'between dates given `undefined` timezone'
|
||||
);
|
||||
});
|
52
api-server/server/utils/getDynamicPropsForUser.js
Normal file
52
api-server/server/utils/getDynamicPropsForUser.js
Normal file
@ -0,0 +1,52 @@
|
||||
|
||||
function getCompletedCertCount(user) {
|
||||
return [
|
||||
'isApisMicroservicesCert',
|
||||
'is2018DataVisCert',
|
||||
'isFrontEndLibsCert',
|
||||
'isInfosecQaCert',
|
||||
'isJsAlgoDataStructCert',
|
||||
'isRespWebDesignCert'
|
||||
].reduce((sum, key) => user[key] ? sum + 1 : sum, 0);
|
||||
}
|
||||
|
||||
function getLegacyCertCount(user) {
|
||||
return [
|
||||
'isFrontEndCert',
|
||||
'isBackEndCert',
|
||||
'isDataVisCert'
|
||||
].reduce((sum, key) => user[key] ? sum + 1 : sum, 0);
|
||||
}
|
||||
|
||||
export default function populateUser(db, user) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let populatedUser = {...user};
|
||||
db.collection('user')
|
||||
.aggregate([
|
||||
{ $match: { _id: user.id } },
|
||||
{ $project: { points: { $size: '$progressTimestamps' } } }
|
||||
], function(err, [{ points = 1 } = {}]) {
|
||||
if (err) { return reject(err); }
|
||||
user.points = points;
|
||||
let completedChallengeCount = 0;
|
||||
let completedProjectCount = 0;
|
||||
if ('completedChallenges' in user) {
|
||||
completedChallengeCount = user.completedChallenges.length;
|
||||
user.completedChallenges.forEach(item => {
|
||||
if (
|
||||
'challengeType' in item &&
|
||||
(item.challengeType === 3 || item.challengeType === 4)
|
||||
) {
|
||||
completedProjectCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
populatedUser.completedChallengeCount = completedChallengeCount;
|
||||
populatedUser.completedProjectCount = completedProjectCount;
|
||||
populatedUser.completedCertCount = getCompletedCertCount(user);
|
||||
populatedUser.completedLegacyCertCount = getLegacyCertCount(user);
|
||||
populatedUser.completedChallenges = [];
|
||||
return resolve(populatedUser);
|
||||
});
|
||||
});
|
||||
}
|
26
api-server/server/utils/index.js
Normal file
26
api-server/server/utils/index.js
Normal file
@ -0,0 +1,26 @@
|
||||
export function dasherize(name) {
|
||||
return ('' + name)
|
||||
.toLowerCase()
|
||||
.replace(/\s/g, '-')
|
||||
.replace(/[^a-z0-9\-\.]/gi, '')
|
||||
.replace(/\:/g, '');
|
||||
}
|
||||
|
||||
export function nameify(str) {
|
||||
return ('' + str)
|
||||
.replace(/[^a-zA-Z0-9\s]/g, '')
|
||||
.replace(/\:/g, '');
|
||||
}
|
||||
|
||||
export function unDasherize(name) {
|
||||
return ('' + name)
|
||||
// replace dash with space
|
||||
.replace(/\-/g, ' ')
|
||||
// strip nonalphanumarics chars except whitespace
|
||||
.replace(/[^a-zA-Z\d\s]/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function addPlaceholderImage(name) {
|
||||
return `https://identicon.org?t=${name}&s=256`;
|
||||
}
|
8
api-server/server/utils/lang-passthrough-urls.js
Normal file
8
api-server/server/utils/lang-passthrough-urls.js
Normal file
@ -0,0 +1,8 @@
|
||||
export default [
|
||||
'auth',
|
||||
'services',
|
||||
'link'
|
||||
].reduce((throughs, route) => {
|
||||
throughs[route] = true;
|
||||
return throughs;
|
||||
}, {});
|
241
api-server/server/utils/map.js
Normal file
241
api-server/server/utils/map.js
Normal file
@ -0,0 +1,241 @@
|
||||
import _ from 'lodash';
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import { unDasherize, nameify } from '../utils';
|
||||
import {
|
||||
addNameIdMap as _addNameIdToMap,
|
||||
checkMapData,
|
||||
getFirstChallenge as _getFirstChallenge
|
||||
} from '../../common/utils/map.js';
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
const isBeta = !!process.env.BETA;
|
||||
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
|
||||
const addNameIdMap = _.once(_addNameIdToMap);
|
||||
const getFirstChallenge = _.once(_getFirstChallenge);
|
||||
/*
|
||||
* interface ChallengeMap {
|
||||
* result: {
|
||||
* superBlocks: [ ...superBlockDashedName: String ]
|
||||
* },
|
||||
* entities: {
|
||||
* superBlock: {
|
||||
* [ ...superBlockDashedName ]: SuperBlock
|
||||
* },
|
||||
* block: {
|
||||
* [ ...blockDashedNameg ]: Block,
|
||||
* challenge: {
|
||||
* [ ...challengeDashedNameg ]: Challenge
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export function _cachedMap({ Block, Challenge }) {
|
||||
const challenges = Challenge.find$({
|
||||
order: [ 'order ASC', 'suborder ASC' ],
|
||||
where: { isPrivate: false }
|
||||
});
|
||||
const challengeMap = challenges
|
||||
.map(
|
||||
challenges => challenges
|
||||
.map(challenge => challenge.toJSON())
|
||||
.reduce((hash, challenge) => {
|
||||
hash[challenge.dashedName] = challenge;
|
||||
return hash;
|
||||
}, {})
|
||||
);
|
||||
const blocks = Block.find$({
|
||||
order: [ 'superOrder ASC', 'order ASC' ],
|
||||
where: { isPrivate: false }
|
||||
});
|
||||
const blockMap = Observable.combineLatest(
|
||||
blocks.map(
|
||||
blocks => blocks
|
||||
.map(block => block.toJSON())
|
||||
.reduce((hash, block) => {
|
||||
hash[block.dashedName] = block;
|
||||
return hash;
|
||||
}, {})
|
||||
),
|
||||
challenges
|
||||
)
|
||||
.map(([ blocksMap, challenges ]) => {
|
||||
return challenges.reduce((blocksMap, challenge) => {
|
||||
if (blocksMap[challenge.block].challenges) {
|
||||
blocksMap[challenge.block].challenges.push(challenge.dashedName);
|
||||
} else {
|
||||
blocksMap[challenge.block] = {
|
||||
...blocksMap[challenge.block],
|
||||
challenges: [ challenge.dashedName ]
|
||||
};
|
||||
}
|
||||
return blocksMap;
|
||||
}, blocksMap);
|
||||
});
|
||||
const superBlockMap = blocks.map(blocks => blocks.reduce((map, block) => {
|
||||
if (
|
||||
map[block.superBlock] &&
|
||||
map[block.superBlock].blocks
|
||||
) {
|
||||
map[block.superBlock].blocks.push(block.dashedName);
|
||||
} else {
|
||||
map[block.superBlock] = {
|
||||
title: _.startCase(block.superBlock),
|
||||
order: block.superOrder,
|
||||
name: nameify(_.startCase(block.superBlock)),
|
||||
dashedName: block.superBlock,
|
||||
blocks: [block.dashedName],
|
||||
message: block.superBlockMessage
|
||||
};
|
||||
}
|
||||
return map;
|
||||
}, {}));
|
||||
const superBlocks = superBlockMap.map(superBlockMap => {
|
||||
return Object.keys(superBlockMap)
|
||||
.map(key => superBlockMap[key])
|
||||
.map(({ dashedName }) => dashedName);
|
||||
});
|
||||
return Observable.combineLatest(
|
||||
superBlockMap,
|
||||
blockMap,
|
||||
challengeMap,
|
||||
superBlocks,
|
||||
(superBlock, block, challenge, superBlocks) => ({
|
||||
entities: {
|
||||
superBlock,
|
||||
block,
|
||||
challenge
|
||||
},
|
||||
result: {
|
||||
superBlocks
|
||||
}
|
||||
})
|
||||
)
|
||||
.do(checkMapData)
|
||||
.shareReplay();
|
||||
}
|
||||
|
||||
export const cachedMap = _.once(_cachedMap);
|
||||
|
||||
// type ObjectId: String;
|
||||
// getChallengeById(
|
||||
// map: Observable[map],
|
||||
// id: ObjectId
|
||||
// ) => Observable[Challenge] | Void;
|
||||
export function getChallengeById(map, id) {
|
||||
return Observable.if(
|
||||
() => !id,
|
||||
map.map(getFirstChallenge),
|
||||
map.map(addNameIdMap)
|
||||
.map(map => {
|
||||
const {
|
||||
entities: { challenge: challengeMap, challengeIdToName }
|
||||
} = map;
|
||||
let finalChallenge;
|
||||
const dashedName = challengeIdToName[id];
|
||||
finalChallenge = challengeMap[dashedName];
|
||||
if (!finalChallenge) {
|
||||
finalChallenge = getFirstChallenge(map);
|
||||
}
|
||||
return finalChallenge;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function getChallengeInfo(map) {
|
||||
return map.map(addNameIdMap)
|
||||
.map(({
|
||||
entities: {
|
||||
challenge: challengeMap,
|
||||
challengeIdToName
|
||||
}
|
||||
}) => ({
|
||||
challengeMap,
|
||||
challengeIdToName
|
||||
}));
|
||||
}
|
||||
|
||||
// if challenge is not isComingSoon or isBeta => load
|
||||
// if challenge is ComingSoon we are in beta||dev => load
|
||||
// if challenge is beta and we are in beta||dev => load
|
||||
// else hide
|
||||
function loadComingSoonOrBetaChallenge({
|
||||
isComingSoon,
|
||||
isBeta: challengeIsBeta
|
||||
}) {
|
||||
return !(isComingSoon || challengeIsBeta) || isDev || isBeta;
|
||||
}
|
||||
|
||||
// this is a hard search
|
||||
// falls back to soft search
|
||||
export function getChallenge(
|
||||
challengeDashedName,
|
||||
blockDashedName,
|
||||
map) {
|
||||
return map
|
||||
.flatMap(({ entities, result: { superBlocks } }) => {
|
||||
const superBlock = entities.superBlock;
|
||||
const block = entities.block[blockDashedName];
|
||||
const challenge = entities.challenge[challengeDashedName];
|
||||
return Observable.if(
|
||||
() => (
|
||||
!blockDashedName ||
|
||||
!block ||
|
||||
!challenge ||
|
||||
!loadComingSoonOrBetaChallenge(challenge)
|
||||
),
|
||||
getChallengeByDashedName(challengeDashedName, map),
|
||||
Observable.just({ block, challenge })
|
||||
)
|
||||
.map(({ challenge, block }) => ({
|
||||
redirect: challenge.block !== blockDashedName ?
|
||||
`/challenges/${block.dashedName}/${challenge.dashedName}` :
|
||||
false,
|
||||
entities: {
|
||||
superBlock,
|
||||
challenge: {
|
||||
[challenge.dashedName]: challenge
|
||||
}
|
||||
},
|
||||
result: {
|
||||
block: block.dashedName,
|
||||
challenge: challenge.dashedName,
|
||||
superBlocks
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
export function getBlockForChallenge(map, challenge) {
|
||||
return map.map(({ entities: { block } }) => block[challenge.block]);
|
||||
}
|
||||
|
||||
export function getChallengeByDashedName(dashedName, map) {
|
||||
const challengeName = unDasherize(dashedName)
|
||||
.replace(challengesRegex, '');
|
||||
const testChallengeName = new RegExp(challengeName, 'i');
|
||||
|
||||
return map
|
||||
.map(({ entities }) => entities.challenge)
|
||||
.flatMap(challengeMap => {
|
||||
return Observable.from(Object.keys(challengeMap))
|
||||
.map(key => challengeMap[key]);
|
||||
})
|
||||
.filter(challenge => {
|
||||
return loadComingSoonOrBetaChallenge(challenge) &&
|
||||
testChallengeName.test(challenge.name);
|
||||
})
|
||||
.last({ defaultValue: null })
|
||||
.flatMap(challengeOrNull => {
|
||||
return Observable.if(
|
||||
() => !!challengeOrNull,
|
||||
Observable.just(challengeOrNull),
|
||||
map.map(getFirstChallenge)
|
||||
);
|
||||
})
|
||||
.flatMap(challenge => {
|
||||
return getBlockForChallenge(map, challenge)
|
||||
.map(block => ({ challenge, block }));
|
||||
});
|
||||
}
|
||||
|
74
api-server/server/utils/middleware.js
Normal file
74
api-server/server/utils/middleware.js
Normal file
@ -0,0 +1,74 @@
|
||||
import dedent from 'dedent';
|
||||
import { validationResult } from 'express-validator/check';
|
||||
|
||||
import { createValidatorErrorFormatter } from './create-handled-error.js';
|
||||
|
||||
export function ifNoUserRedirectTo(url, message, type = 'errors') {
|
||||
return function(req, res, next) {
|
||||
const { path } = req;
|
||||
if (req.user) {
|
||||
return next();
|
||||
}
|
||||
|
||||
req.flash(type, message || `You must be signed in to access ${path}`);
|
||||
|
||||
return res.redirect(url);
|
||||
};
|
||||
}
|
||||
|
||||
export function ifNoUserSend(sendThis) {
|
||||
return function(req, res, next) {
|
||||
if (req.user) {
|
||||
return next();
|
||||
}
|
||||
return res.status(200).send(sendThis);
|
||||
};
|
||||
}
|
||||
|
||||
export function ifNoUser401(req, res, next) {
|
||||
if (req.user) {
|
||||
return next();
|
||||
}
|
||||
return res.status(401).end();
|
||||
}
|
||||
|
||||
export function ifNotVerifiedRedirectToUpdateEmail(req, res, next) {
|
||||
const { user } = req;
|
||||
if (!user) {
|
||||
return next();
|
||||
}
|
||||
if (!user.emailVerified) {
|
||||
req.flash(
|
||||
'danger',
|
||||
dedent`
|
||||
We do not have your verified email address on record,
|
||||
please add it in the settings to continue with your request.
|
||||
`
|
||||
);
|
||||
return res.redirect('/settings');
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
export function ifUserRedirectTo(path = '/', status) {
|
||||
status = status === 302 ? 302 : 301;
|
||||
return (req, res, next) => {
|
||||
if (req.user) {
|
||||
return res.status(status).redirect(path);
|
||||
}
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
// for use with express-validator error formatter
|
||||
export const createValidatorErrorHandler = (...args) => (req, res, next) => {
|
||||
const validation = validationResult(req)
|
||||
.formatWith(createValidatorErrorFormatter(...args));
|
||||
|
||||
if (!validation.isEmpty()) {
|
||||
const errors = validation.array();
|
||||
return next(errors.pop());
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
79
api-server/server/utils/publicUserProps.js
Normal file
79
api-server/server/utils/publicUserProps.js
Normal file
@ -0,0 +1,79 @@
|
||||
import { isURL } from 'validator';
|
||||
|
||||
import { addPlaceholderImage } from './';
|
||||
import {
|
||||
prepUniqueDaysByHours,
|
||||
calcCurrentStreak,
|
||||
calcLongestStreak
|
||||
} from '../utils/user-stats';
|
||||
|
||||
export const publicUserProps = [
|
||||
'about',
|
||||
'calendar',
|
||||
'completedChallenges',
|
||||
'githubProfile',
|
||||
'isApisMicroservicesCert',
|
||||
'isBackEndCert',
|
||||
'isCheater',
|
||||
'isDonating',
|
||||
'is2018DataVisCert',
|
||||
'isDataVisCert',
|
||||
'isFrontEndCert',
|
||||
'isFullStackCert',
|
||||
'isFrontEndLibsCert',
|
||||
'isHonest',
|
||||
'isInfosecQaCert',
|
||||
'isJsAlgoDataStructCert',
|
||||
'isRespWebDesignCert',
|
||||
'linkedin',
|
||||
'location',
|
||||
'name',
|
||||
'points',
|
||||
'portfolio',
|
||||
'profileUI',
|
||||
'projects',
|
||||
'streak',
|
||||
'twitter',
|
||||
'username',
|
||||
'website',
|
||||
'yearsTopContributor'
|
||||
];
|
||||
|
||||
export const userPropsForSession = [
|
||||
...publicUserProps,
|
||||
'currentChallengeId',
|
||||
'email',
|
||||
'emailVerified',
|
||||
'id',
|
||||
'sendQuincyEmail',
|
||||
'theme',
|
||||
'completedChallengeCount',
|
||||
'completedProjectCount',
|
||||
'completedCertCount',
|
||||
'completedLegacyCertCount',
|
||||
'acceptedPrivacyTerms'
|
||||
];
|
||||
|
||||
export function normaliseUserFields(user) {
|
||||
const about = user.bio && !user.about ? user.bio : user.about;
|
||||
const picture = user.picture || addPlaceholderImage(user.username);
|
||||
const twitter = user.twitter && isURL(user.twitter) ?
|
||||
user.twitter :
|
||||
user.twitter && `https://www.twitter.com/${user.twitter.replace(/^@/, '')}`;
|
||||
return { about, picture, twitter };
|
||||
}
|
||||
|
||||
export function getProgress(progressTimestamps, timezone = 'EST') {
|
||||
const calendar = progressTimestamps
|
||||
.filter(Boolean)
|
||||
.reduce((data, timestamp) => {
|
||||
data[Math.floor(timestamp / 1000)] = 1;
|
||||
return data;
|
||||
}, {});
|
||||
const uniqueHours = prepUniqueDaysByHours(progressTimestamps, timezone);
|
||||
const streak = {
|
||||
longest: calcLongestStreak(uniqueHours, timezone),
|
||||
current: calcCurrentStreak(uniqueHours, timezone)
|
||||
};
|
||||
return { calendar, streak };
|
||||
}
|
6
api-server/server/utils/react.js
vendored
Normal file
6
api-server/server/utils/react.js
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
export const errorThrowerMiddleware = () => next => action => {
|
||||
if (action.error) {
|
||||
throw action.payload;
|
||||
}
|
||||
return next(action);
|
||||
};
|
125
api-server/server/utils/resources.json
Normal file
125
api-server/server/utils/resources.json
Normal file
@ -0,0 +1,125 @@
|
||||
{
|
||||
"verbs": [
|
||||
"aced",
|
||||
"nailed",
|
||||
"rocked",
|
||||
"destroyed",
|
||||
"owned",
|
||||
"crushed",
|
||||
"conquered",
|
||||
"shredded",
|
||||
"demolished",
|
||||
"devoured",
|
||||
"banished",
|
||||
"wrangled"
|
||||
],
|
||||
"compliments": [
|
||||
"Over the top!",
|
||||
"Down the rabbit hole we go!",
|
||||
"Bring that rain!",
|
||||
"Target acquired!",
|
||||
"Feel that need for speed!",
|
||||
"You've got guts!",
|
||||
"We have liftoff!",
|
||||
"To infinity and beyond!",
|
||||
"Encore!",
|
||||
"Onward, ho!",
|
||||
"Challenge destroyed!",
|
||||
"It's on like Donkey Kong!",
|
||||
"Power level? It's over 9000!",
|
||||
"Coding spree!",
|
||||
"Code long and prosper.",
|
||||
"The crowd goes wild!",
|
||||
"One for the guinness book!",
|
||||
"Flawless victory!",
|
||||
"Most efficient!",
|
||||
"Party on, Wayne!",
|
||||
"You've got the touch!",
|
||||
"You're on fire!",
|
||||
"Don't hurt 'em, Hammer!",
|
||||
"The town is now red!",
|
||||
"To the nines!",
|
||||
"The world rejoices!",
|
||||
"That's the way it's done!",
|
||||
"You rock!",
|
||||
"Woo-hoo!",
|
||||
"We knew you could do it!",
|
||||
"Hyper Combo Finish!",
|
||||
"Nothing but net!",
|
||||
"Boom-shakalaka!",
|
||||
"You're a shooting star!",
|
||||
"You're unstoppable!",
|
||||
"Way cool!",
|
||||
"You're king of the world!",
|
||||
"Walk on that sunshine!",
|
||||
"Keep on trucking!",
|
||||
"Off the charts!",
|
||||
"There is no spoon!",
|
||||
"Cranked it up to 11!",
|
||||
"Escape velocity reached!",
|
||||
"You make this look easy!",
|
||||
"Passed with flying colors!",
|
||||
"You've got this!",
|
||||
"Happy, happy, joy, joy!",
|
||||
"Tomorrow, the world!",
|
||||
"Your powers combined!",
|
||||
"A winner is you!",
|
||||
"It's alive. It's alive!",
|
||||
"Sonic Boom!",
|
||||
"Here's looking at you, Code!",
|
||||
"Ride like the wind!",
|
||||
"Legen - wait for it - dary!",
|
||||
"Ludicrous Speed! Go!",
|
||||
"Yes we can!",
|
||||
"Most triumphant!",
|
||||
"One loop to rule them all!",
|
||||
"By the power of Grayskull!",
|
||||
"You did it!",
|
||||
"Storm that castle!",
|
||||
"Face-melting guitar solo!",
|
||||
"Checkmate!",
|
||||
"Bodacious!",
|
||||
"Tubular!",
|
||||
"You're outta sight!",
|
||||
"Keep calm and code on!",
|
||||
"Even sad panda smiles!",
|
||||
"Even grumpy cat approves!",
|
||||
"Kool-Aid Man says oh yeah!",
|
||||
"Bullseye!",
|
||||
"Far out!",
|
||||
"You're heating up!",
|
||||
"Hasta la vista, challenge!",
|
||||
"Terminated.",
|
||||
"Off the hook!",
|
||||
"Thundercats, Hooo!",
|
||||
"Shiver me timbers!",
|
||||
"Raise the roof!",
|
||||
"I also live dangerously.",
|
||||
"Get to the choppa!",
|
||||
"Bingo!",
|
||||
"And you're all out of gum.",
|
||||
"Even honeybadger cares!",
|
||||
"Helm, Warp Nine. Engage!",
|
||||
"Gotta code 'em all!",
|
||||
"Spool up the FTL drive!",
|
||||
"Cool beans!",
|
||||
"They're in another castle.",
|
||||
"Power UP!",
|
||||
"Pikachu chooses you!",
|
||||
"We're gonna pump you up!",
|
||||
"I gotta have more cow bell."
|
||||
],
|
||||
"phrases": [
|
||||
"Shout it from on top of a mountain",
|
||||
"Tell everyone and their dogs",
|
||||
"Show them. Show them all!",
|
||||
"Inspire your friends",
|
||||
"Tell the world of your greatness",
|
||||
"Look accomplished on social media",
|
||||
"Share news of your grand endeavor",
|
||||
"Establish your alibi for the past two hours",
|
||||
"Prove to mom that computers aren't just for games",
|
||||
"With coding power comes sharing responsibility",
|
||||
"Have you told your friends of your coding powers?"
|
||||
]
|
||||
}
|
58
api-server/server/utils/rx.js
Normal file
58
api-server/server/utils/rx.js
Normal file
@ -0,0 +1,58 @@
|
||||
import Rx, { AsyncSubject, Observable } from 'rx';
|
||||
import moment from 'moment';
|
||||
import debugFactory from 'debug';
|
||||
|
||||
const debug = debugFactory('fcc:rxUtils');
|
||||
|
||||
export function saveInstance(instance) {
|
||||
return new Rx.Observable.create(function(observer) {
|
||||
if (!instance || typeof instance.save !== 'function') {
|
||||
debug('no instance or save method');
|
||||
observer.onNext();
|
||||
return observer.onCompleted();
|
||||
}
|
||||
return instance.save(function(err, savedInstance) {
|
||||
if (err) {
|
||||
return observer.onError(err);
|
||||
}
|
||||
debug('instance saved');
|
||||
observer.onNext(savedInstance);
|
||||
return observer.onCompleted();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// alias saveInstance
|
||||
export const saveUser = saveInstance;
|
||||
|
||||
// observeQuery(Model: Object, methodName: String, query: Any) => Observable
|
||||
export function observeQuery(Model, methodName, query) {
|
||||
return Rx.Observable.fromNodeCallback(Model[methodName], Model)(query);
|
||||
}
|
||||
|
||||
// observeMethod(
|
||||
// context: Object, methodName: String
|
||||
// ) => (query: Any) => Observable
|
||||
export function observeMethod(context, methodName) {
|
||||
return Rx.Observable.fromNodeCallback(context[methodName], context);
|
||||
}
|
||||
|
||||
// must be bound to an observable instance
|
||||
// timeCache(amount: Number, unit: String) => Observable
|
||||
export function timeCache(time, unit) {
|
||||
const source = this;
|
||||
let cache;
|
||||
let expireCacheAt;
|
||||
return Observable.create(observable => {
|
||||
// if there is no expire time set
|
||||
// or if expireCacheAt is smaller than now,
|
||||
// set new expire time in MS and create new subscription to source
|
||||
if (!expireCacheAt || expireCacheAt < Date.now()) {
|
||||
// set expire in ms;
|
||||
expireCacheAt = moment().add(time, unit).valueOf();
|
||||
cache = new AsyncSubject();
|
||||
source.subscribe(cache);
|
||||
}
|
||||
return cache.subscribe(observable);
|
||||
});
|
||||
}
|
19
api-server/server/utils/superBlockCertTypeMap.js
Normal file
19
api-server/server/utils/superBlockCertTypeMap.js
Normal file
@ -0,0 +1,19 @@
|
||||
import certTypes from './certTypes.json';
|
||||
|
||||
const superBlockCertTypeMap = {
|
||||
// legacy
|
||||
'legacy-front-end': certTypes.frontEnd,
|
||||
'legacy-back-end': certTypes.backEnd,
|
||||
'legacy-data-visualization': certTypes.dataVis,
|
||||
|
||||
// modern
|
||||
'responsive-web-design': certTypes.respWebDesign,
|
||||
'javascript-algorithms-and-data-structures': certTypes.jsAlgoDataStruct,
|
||||
'front-end-libraries': certTypes.frontEndLibs,
|
||||
'data-visualization': certTypes.dataVis2018,
|
||||
'apis-and-microservices': certTypes.apisMicroservices,
|
||||
'information-security-and-quality-assurance': certTypes.infosecQa,
|
||||
'full-stack': certTypes.fullStack
|
||||
};
|
||||
|
||||
export default superBlockCertTypeMap;
|
37
api-server/server/utils/url-utils.js
Normal file
37
api-server/server/utils/url-utils.js
Normal file
@ -0,0 +1,37 @@
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
const isBeta = !!process.env.BETA;
|
||||
|
||||
export function getEmailSender() {
|
||||
return process.env.SES_MAIL_FROM || 'team@freecodecamp.org';
|
||||
}
|
||||
|
||||
export function getPort() {
|
||||
if (!isDev) {
|
||||
return '443';
|
||||
}
|
||||
return process.env.SYNC_PORT || '3000';
|
||||
}
|
||||
|
||||
export function getProtocol() {
|
||||
return isDev ? 'http' : 'https';
|
||||
}
|
||||
|
||||
export function getHost() {
|
||||
if (isDev) {
|
||||
return process.env.HOST || 'localhost';
|
||||
}
|
||||
return isBeta ? 'beta.freecodecamp.org' : 'www.freecodecamp.org';
|
||||
}
|
||||
|
||||
export function getServerFullURL() {
|
||||
if (!isDev) {
|
||||
return getProtocol()
|
||||
+ '://'
|
||||
+ getHost();
|
||||
}
|
||||
return getProtocol()
|
||||
+ '://'
|
||||
+ getHost()
|
||||
+ ':'
|
||||
+ getPort();
|
||||
}
|
91
api-server/server/utils/user-stats.js
Normal file
91
api-server/server/utils/user-stats.js
Normal file
@ -0,0 +1,91 @@
|
||||
import compose from 'lodash/fp/compose';
|
||||
import map from 'lodash/fp/map';
|
||||
import sortBy from 'lodash/fp/sortBy';
|
||||
import trans from 'lodash/fp/transform';
|
||||
import last from 'lodash/fp/last';
|
||||
import forEachRight from 'lodash/fp/forEachRight';
|
||||
import moment from 'moment-timezone';
|
||||
import { dayCount } from '../utils/date-utils';
|
||||
|
||||
const transform = trans.convert({ cap: false });
|
||||
|
||||
const hoursBetween = 24;
|
||||
const hoursDay = 24;
|
||||
|
||||
export function prepUniqueDaysByHours(cals, tz = 'UTC') {
|
||||
|
||||
let prev = null;
|
||||
|
||||
// compose goes bottom to top (map > sortBy > transform)
|
||||
return compose(
|
||||
transform((data, cur, i) => {
|
||||
if (i < 1) {
|
||||
data.push(cur);
|
||||
prev = cur;
|
||||
} else if (
|
||||
moment(cur)
|
||||
.tz(tz)
|
||||
.diff(moment(prev).tz(tz).startOf('day'), 'hours')
|
||||
>= hoursDay
|
||||
) {
|
||||
data.push(cur);
|
||||
prev = cur;
|
||||
}
|
||||
}, []),
|
||||
sortBy(e => e),
|
||||
map(ts => moment(ts).tz(tz).startOf('hours').valueOf())
|
||||
)(cals);
|
||||
}
|
||||
|
||||
export function calcCurrentStreak(cals, tz = 'UTC') {
|
||||
|
||||
let prev = last(cals);
|
||||
if (
|
||||
moment()
|
||||
.tz(tz)
|
||||
.startOf('day')
|
||||
.diff(moment(prev).tz(tz), 'hours')
|
||||
> hoursBetween
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
let currentStreak = 0;
|
||||
let streakContinues = true;
|
||||
forEachRight(cur => {
|
||||
if (
|
||||
moment(prev)
|
||||
.tz(tz)
|
||||
.startOf('day')
|
||||
.diff(moment(cur).tz(tz), 'hours')
|
||||
<= hoursBetween
|
||||
) {
|
||||
prev = cur;
|
||||
currentStreak++;
|
||||
} else {
|
||||
// current streak found
|
||||
streakContinues = false;
|
||||
}
|
||||
return streakContinues;
|
||||
})(cals);
|
||||
|
||||
return currentStreak;
|
||||
}
|
||||
|
||||
export function calcLongestStreak(cals, tz = 'UTC') {
|
||||
|
||||
let tail = cals[0];
|
||||
const longest = cals.reduce((longest, head, index) => {
|
||||
const last = cals[index === 0 ? 0 : index - 1];
|
||||
// is streak broken
|
||||
if (moment(head).tz(tz).startOf('day').diff(moment(last).tz(tz), 'hours')
|
||||
> hoursBetween) {
|
||||
tail = head;
|
||||
}
|
||||
if (dayCount(longest, tz) < dayCount([head, tail], tz)) {
|
||||
return [head, tail];
|
||||
}
|
||||
return longest;
|
||||
}, [cals[0], cals[0]]);
|
||||
|
||||
return dayCount(longest, tz);
|
||||
}
|
429
api-server/server/utils/user-stats.test.js
Normal file
429
api-server/server/utils/user-stats.test.js
Normal file
@ -0,0 +1,429 @@
|
||||
import test from 'tape';
|
||||
import moment from 'moment-timezone';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {
|
||||
prepUniqueDaysByHours,
|
||||
calcCurrentStreak,
|
||||
calcLongestStreak
|
||||
} from './user-stats';
|
||||
|
||||
// setting now to 2016-02-03T11:00:00 (PST)
|
||||
const clock = sinon.useFakeTimers(1454526000000);
|
||||
const PST = 'America/Los_Angeles';
|
||||
|
||||
test('Prepare calendar items', function(t) {
|
||||
|
||||
t.plan(5);
|
||||
|
||||
t.deepEqual(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 14:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 20:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
[1438567200000],
|
||||
'should return correct epoch when all entries fall into one day in UTC'
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
[1438567200000],
|
||||
'should return correct epoch when given two identical dates'
|
||||
);
|
||||
|
||||
|
||||
t.deepEqual(
|
||||
prepUniqueDaysByHours([
|
||||
// 8/2/2015 in America/Los_Angeles
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 14:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 20:00', 'M/D/YYYY H:mm').valueOf()
|
||||
], PST),
|
||||
[1438567200000, 1438610400000],
|
||||
'should return 2 epochs when dates fall into two days in PST'
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/1/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('3/3/2015 14:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/30/2014 20:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
[1412107200000, 1425391200000, 1438394400000],
|
||||
'should return 3 epochs when dates fall into three days'
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
prepUniqueDaysByHours([
|
||||
1438387200000, 1425340800000, 1412035200000
|
||||
]),
|
||||
[1412035200000, 1425340800000, 1438387200000],
|
||||
'should return same but sorted array if all input dates are start of day'
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
test('Current streak calculation', function(t) {
|
||||
|
||||
t.plan(11);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
1,
|
||||
'should return 1 day when today one challenge was completed'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
1,
|
||||
'should return 1 day when today more than one challenge was completed'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
),
|
||||
0,
|
||||
'should return 0 day when today 0 challenges were completed'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
2,
|
||||
'should return 2 days when today and yesterday challenges were completed'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc(moment.utc().subtract(2, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
3,
|
||||
'should return 3 when today and for two days before user was activity'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(47, 'hours')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(11, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
1,
|
||||
'should return 1 when there is 1.5 days long break and ' +
|
||||
'dates fall into two days separated by third'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(40, 'hours')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
2,
|
||||
'should return 2 when the break is more than 1.5 days ' +
|
||||
'but dates fall into two consecutive days'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
1,
|
||||
'should return correct count in default timezone UTC ' +
|
||||
'given `undefined` timezone'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
], PST),
|
||||
PST
|
||||
),
|
||||
2,
|
||||
'should return 2 days when today and yesterday ' +
|
||||
'challenges were completed given PST'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
1453174506164, 1453175436725, 1453252466853, 1453294968225,
|
||||
1453383782844, 1453431903117, 1453471373080, 1453594733026,
|
||||
1453645014058, 1453746762747, 1453747659197, 1453748029416,
|
||||
1453818029213, 1453951796007, 1453988570615, 1454069704441,
|
||||
1454203673979, 1454294055498, 1454333545125, 1454415163903,
|
||||
1454519128123, moment.tz(PST).valueOf()
|
||||
], PST),
|
||||
PST
|
||||
),
|
||||
17,
|
||||
'should return 17 when there is no break in given timezone ' +
|
||||
'(but would be the break if in UTC)'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
1453174506164, 1453175436725, 1453252466853, 1453294968225,
|
||||
1453383782844, 1453431903117, 1453471373080, 1453594733026,
|
||||
1453645014058, 1453746762747, 1453747659197, 1453748029416,
|
||||
1453818029213, 1453951796007, 1453988570615, 1454069704441,
|
||||
1454203673979, 1454294055498, 1454333545125, 1454415163903,
|
||||
1454519128123, moment.utc().valueOf()
|
||||
])
|
||||
),
|
||||
4,
|
||||
'should return 4 when there is a break in UTC ' +
|
||||
'(but would be no break in PST)'
|
||||
);
|
||||
});
|
||||
|
||||
test('Longest streak calculation', function(t) {
|
||||
t.plan(14);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/12/2015 4:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
),
|
||||
1,
|
||||
'should return 1 when there is the only one one-day-long streak available'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 1:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
),
|
||||
4,
|
||||
'should return 4 when there is the only one ' +
|
||||
'more-than-one-days-long streak available'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
1,
|
||||
'should return 1 when there is only one one-day-long streak and it is today'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
2,
|
||||
'should return 2 when yesterday and today makes longest streak'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/4/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/5/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/6/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/7/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('11/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
),
|
||||
4,
|
||||
'should return 4 when there is a month long break'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 15:30', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc(moment.utc('9/12/2015 15:30', 'M/D/YYYY H:mm')
|
||||
.add(37, 'hours')).valueOf(),
|
||||
|
||||
moment.utc('9/14/2015 22:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/15/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
),
|
||||
2,
|
||||
'should return 2 when there is a more than 1.5 days ' +
|
||||
'long break of (36 hours)'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc(moment.utc().subtract(2, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc().valueOf()
|
||||
])
|
||||
),
|
||||
4,
|
||||
'should return 4 when the longest streak consist of ' +
|
||||
'several same day timestamps'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/13/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/14/2015 5:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
),
|
||||
4,
|
||||
'should return 4 when there are several longest streaks (same length)'
|
||||
);
|
||||
|
||||
let cals = [];
|
||||
const n = 100;
|
||||
for (var i = 0; i < n; i++) {
|
||||
cals.push(moment.utc(moment.utc().subtract(i, 'days')).valueOf());
|
||||
}
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(prepUniqueDaysByHours(cals)),
|
||||
n,
|
||||
'should return correct longest streak when there is a very long period'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
2,
|
||||
'should return correct longest streak in default timezone ' +
|
||||
'UTC given `undefined` timezone'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 1:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]), PST
|
||||
),
|
||||
4,
|
||||
'should return 4 when there is the only one more-than-one-days-long ' +
|
||||
'streak available given PST'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/11/2015 23:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 6:00', 'M/D/YYYY H:mm').valueOf()
|
||||
], PST), PST
|
||||
),
|
||||
3,
|
||||
'should return 3 when longest streak is 3 in PST ' +
|
||||
'(but would be different in default timezone UTC)'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
1453174506164, 1453175436725, 1453252466853, 1453294968225,
|
||||
1453383782844, 1453431903117, 1453471373080, 1453594733026,
|
||||
1453645014058, 1453746762747, 1453747659197, 1453748029416,
|
||||
1453818029213, 1453951796007, 1453988570615, 1454069704441,
|
||||
1454203673979, 1454294055498, 1454333545125, 1454415163903,
|
||||
1454519128123, moment.tz(PST).valueOf()
|
||||
], PST),
|
||||
PST
|
||||
),
|
||||
17,
|
||||
'should return 17 when there is no break in PST ' +
|
||||
'(but would be break in UTC) and it is current'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
1453174506164, 1453175436725, 1453252466853, 1453294968225,
|
||||
1453383782844, 1453431903117, 1453471373080, 1453594733026,
|
||||
1453645014058, 1453746762747, 1453747659197, 1453748029416,
|
||||
1453818029213, 1453951796007, 1453988570615, 1454069704441,
|
||||
1454203673979, 1454294055498, 1454333545125, 1454415163903,
|
||||
1454519128123, moment.utc().valueOf()
|
||||
])
|
||||
),
|
||||
4,
|
||||
'should return 4 when there is a break in UTC (but no break in PST)'
|
||||
);
|
||||
});
|
||||
|
||||
test.onFinish(() => {
|
||||
clock.restore();
|
||||
});
|
20
api-server/server/views/404.jade
Normal file
20
api-server/server/views/404.jade
Normal file
@ -0,0 +1,20 @@
|
||||
extends ./layout
|
||||
|
||||
block content
|
||||
.spacer
|
||||
.row
|
||||
.col-sm-6.col-sm-offset-3
|
||||
hr
|
||||
.text-center
|
||||
h1 404
|
||||
.row
|
||||
.col-sm-6.col-sm-offset-3
|
||||
.text-center
|
||||
h2 Ooops, we couldn't find that page.
|
||||
.spacer
|
||||
.row
|
||||
.col-sm-6.col-sm-offset-3
|
||||
.text-center
|
||||
a.btn.signup-btn.btn-block(href='/map') Head to the Map
|
||||
hr
|
||||
.spacer
|
121
api-server/server/views/account/accept-privacy-terms.jade
Normal file
121
api-server/server/views/account/accept-privacy-terms.jade
Normal file
@ -0,0 +1,121 @@
|
||||
extends ../layout
|
||||
block content
|
||||
.row.flashMessage.negative-30
|
||||
.col-xs-12.col-sm-8.col-sm-offset-2.col-md-6.col-md-offset-3
|
||||
#flash-board.alert.fade.in(style='display: none;')
|
||||
button.close(type='button', data-dismiss='alert')
|
||||
i.fas.fa-times-circle#flash-close
|
||||
#flash-content
|
||||
.row
|
||||
.col-xs-12
|
||||
#accept-privacy-terms
|
||||
.row
|
||||
.text-center
|
||||
br
|
||||
br
|
||||
h3 Please review our updated privacy policy and the terms of service.
|
||||
br
|
||||
.row
|
||||
.col-sm-6.col-sm-offset-3
|
||||
form(method='POST', action='/update-privacy-terms')
|
||||
input(type='hidden', name='_csrf', value=_csrf)
|
||||
.checkbox
|
||||
label
|
||||
input(id='terms', name='privacy', type='checkbox')
|
||||
span.cr
|
||||
i.cr-icon.fa.fa-check
|
||||
| I accept the
|
||||
a(href='https://www.freecodecamp.org/terms' target='_blank') terms of service
|
||||
| (required).
|
||||
.checkbox
|
||||
label
|
||||
input(id='privacy', name='privacy', type='checkbox')
|
||||
span.cr
|
||||
i.cr-icon.fa.fa-check
|
||||
| I accept the
|
||||
a(href='https://www.freecodecamp.org/privacy' target='_blank') privacy policy
|
||||
| (required).
|
||||
.checkbox
|
||||
label
|
||||
input(id='quincyemails', name='quincyemails', type='checkbox')
|
||||
span.cr
|
||||
i.cr-icon.fa.fa-check
|
||||
| I want weekly emails from Quincy (freeCodeCamp.org's founder).
|
||||
.button-spacer
|
||||
button.btn.btn-primary.btn-lg.btn-block(id='submit-button', type='submit')
|
||||
.row
|
||||
.col-sm-6.col-sm-offset-3
|
||||
a.btn.btn-primary.btn-lg.btn-block(id='continue-button', href='/', style='display: none;')
|
||||
| Continue to freeCodeCamp
|
||||
|
||||
include ../homePartials/scripts
|
||||
script.
|
||||
$(document).ready(function() {
|
||||
var checkedBoxCount = 0;
|
||||
function disableContinueButtonForAgreement(isLaunched) {
|
||||
if (isLaunched) {
|
||||
$('#submit-button').prop('disabled', true).html(
|
||||
'<span style="color:#E0E0E0;">Submit<\/span>');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLaunched && checkedBoxCount === 2){
|
||||
$('#submit-button').prop('disabled', false).html(
|
||||
'<span>Submit<\/span>');
|
||||
}
|
||||
}
|
||||
disableContinueButtonForAgreement(true);
|
||||
$('#terms').click(function() {
|
||||
if (this.checked) {
|
||||
checkedBoxCount++;
|
||||
disableContinueButtonForAgreement(false);
|
||||
} else {
|
||||
checkedBoxCount--;
|
||||
disableContinueButtonForAgreement(true);
|
||||
}
|
||||
});
|
||||
$('#privacy').click(function() {
|
||||
if (this.checked) {
|
||||
checkedBoxCount++;
|
||||
disableContinueButtonForAgreement(false);
|
||||
} else {
|
||||
checkedBoxCount--;
|
||||
disableContinueButtonForAgreement(true);
|
||||
}
|
||||
});
|
||||
$('form').submit(function(event){
|
||||
event.preventDefault();
|
||||
$('#flash-board').hide();
|
||||
var $form = $(event.target);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : $form.attr('action'),
|
||||
data : $form.serialize(),
|
||||
dataType : 'json',
|
||||
encode : true,
|
||||
xhrFields : { withCredentials: true }
|
||||
})
|
||||
.fail(error => {
|
||||
if (error.responseText){
|
||||
var data = JSON.parse(error.responseText);
|
||||
if(data.message)
|
||||
$('#flash-content').html(data.message);
|
||||
$('#flash-board')
|
||||
.removeClass('alert-success')
|
||||
.addClass('alert-info')
|
||||
.fadeIn();
|
||||
}
|
||||
})
|
||||
.done(data =>{
|
||||
if(data && data.message){
|
||||
$('#flash-content').html(data.message);
|
||||
$('#flash-board')
|
||||
.removeClass('alert-info')
|
||||
.addClass('alert-success')
|
||||
.fadeIn();
|
||||
$('#accept-privacy-terms').hide();
|
||||
$('#continue-button').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
17
api-server/server/views/account/beta.jade
Normal file
17
api-server/server/views/account/beta.jade
Normal file
@ -0,0 +1,17 @@
|
||||
extends ../layout
|
||||
block content
|
||||
.text-center
|
||||
h2 Welcome to the freeCodeCamp beta.
|
||||
h3 We have disabled new sign ups on beta.
|
||||
.text-left.col-xs-12.col-sm-8.col-sm-offset-2.col-md-6.col-md-offset-3
|
||||
p You can try our new beta challenges, but your progress <bold>will not</bold> be saved. 
|
||||
a(href='/map') Click here to go to the beta challenges
|
||||
| .
|
||||
p Or you can return to our stable challenges, where your progress <bold>will</bold> be saved. 
|
||||
a(href='https://www.freecodecamp.org') Click here to go to the stable challenges
|
||||
| .
|
||||
p If you want to learn more about this beta, read our 
|
||||
a(href='https://forum.freecodecamp.org/t/frequently-asked-questions-about-the-freecodecamp-beta/134331') Frequently Asked Questions
|
||||
| .
|
||||
|
||||
p Happy coding!
|
28
api-server/server/views/account/delete.jade
Normal file
28
api-server/server/views/account/delete.jade
Normal file
@ -0,0 +1,28 @@
|
||||
extends ../layout
|
||||
block content
|
||||
include ../partials/flyer
|
||||
#modal-dialog.modal
|
||||
.modal-dialog
|
||||
.modal-content
|
||||
.modal-header
|
||||
a.close(href='/settings', data-dismiss='modal', aria-hidden='true') ×
|
||||
h3 You don't really want to delete your account, do you?
|
||||
.modal-body
|
||||
p This will really delete all your data, including all your progress and brownie points.
|
||||
p We won't be able to recover any of it for you later, even if you change your mind.
|
||||
p If there's something we could do better, send us an email instead and we'll do our best:  
|
||||
a(href="mailto:team@freecodecamp.org") team@freecodecamp.org
|
||||
| .
|
||||
.modal-footer
|
||||
a.btn.btn-success.btn-block(href='/settings', data-dismiss='modal', aria-hidden='true')
|
||||
| Nevermind, I don't want to delete all of my progress
|
||||
.spacer
|
||||
form(action='/account/delete', method='POST')
|
||||
input(type='hidden', name='_csrf', value=_csrf)
|
||||
button.btn.btn-danger.btn-block(type='submit')
|
||||
| I am 100% sure I want to delete my account and all of my progress
|
||||
script.
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const modal$ = document.getElementById('modal-dialog');
|
||||
modal$.classList.add('show');
|
||||
});
|
29
api-server/server/views/account/report-profile.jade
Normal file
29
api-server/server/views/account/report-profile.jade
Normal file
@ -0,0 +1,29 @@
|
||||
extends ../layout
|
||||
block content
|
||||
#modal-dialog.modal
|
||||
.modal-dialog
|
||||
.modal-content
|
||||
.modal-header
|
||||
a.close(href='/settings', data-dismiss='modal', aria-hidden='true') ×
|
||||
h3 Do you want to report #{username}'s profile for abuse?
|
||||
.modal-body
|
||||
p We will notify the community moderators' team,
|
||||
| and a send copy of this report to your email:
|
||||
strong #{user.email}
|
||||
| . We may get back to you for more information, if required.
|
||||
.modal-footer
|
||||
form(action='/' + username +'/report-user/', method='POST')
|
||||
input(type='hidden', name='_csrf', value=_csrf)
|
||||
div
|
||||
textarea.modal-textarea(name='reportDescription', cols='40', rows='5')
|
||||
.spacer
|
||||
button.btn.btn-danger.btn-block(type='submit')
|
||||
| Yes, submit my report about this user's profile.
|
||||
.spacer
|
||||
a.btn.btn-success.btn-block(href='/settings', data-dismiss='modal', aria-hidden='true')
|
||||
| Nevermind, I don't want to report this user.
|
||||
script.
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const modal$ = document.getElementById('modal-dialog');
|
||||
modal$.classList.add('show');
|
||||
});
|
25
api-server/server/views/account/reset-progress.jade
Normal file
25
api-server/server/views/account/reset-progress.jade
Normal file
@ -0,0 +1,25 @@
|
||||
extends ../layout
|
||||
block content
|
||||
include ../partials/flyer
|
||||
#modal-dialog.modal
|
||||
.modal-dialog
|
||||
.modal-content
|
||||
.modal-header
|
||||
a.close(href='/settings', data-dismiss='modal', aria-hidden='true') ×
|
||||
h3 You don't really want to reset your progress, do you?
|
||||
.modal-body
|
||||
p This will really delete all of your progress and brownie points.
|
||||
p We won't be able to recover any of it for you later, even if you change your mind.
|
||||
.modal-footer
|
||||
a.btn.btn-success.btn-block(href='/settings', data-dismiss='modal', aria-hidden='true')
|
||||
| Nevermind, I don't want to delete all of my progress and brownie points
|
||||
.spacer
|
||||
form(action='/account/resetprogress', method='POST')
|
||||
input(type='hidden', name='_csrf', value=_csrf)
|
||||
button.btn.btn-danger.btn-block(type='submit')
|
||||
| I am 100% sure I want to reset all of my progress and brownie points
|
||||
script.
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const modal$ = document.getElementById('modal-dialog');
|
||||
modal$.classList.add('show');
|
||||
});
|
188
api-server/server/views/account/show.jade
Normal file
188
api-server/server/views/account/show.jade
Normal file
@ -0,0 +1,188 @@
|
||||
extends ../layout
|
||||
block content
|
||||
include ../partials/flyer
|
||||
.app-content.app-centered
|
||||
script.
|
||||
var challengeName = 'Profile View';
|
||||
if (user && user.username === username)
|
||||
.row
|
||||
.col-xs-12
|
||||
a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='/settings')
|
||||
| Update my settings
|
||||
.col-xs-12
|
||||
a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='/signout')
|
||||
| Sign me out of freeCodeCamp
|
||||
.col-xs-12
|
||||
a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='mailto:team@freecodecamp.org')
|
||||
| Email us at team@freecodecamp.org
|
||||
.spacer
|
||||
h1.text-center #{username}'s code portfolio
|
||||
hr
|
||||
.row
|
||||
.col-xs-12.col-sm-10.col-sm-offset-1.col-md-8.col-md-offset-2.text-center
|
||||
if picture
|
||||
img.img-center.img-responsive.public-profile-img.border-radius-5(src=picture)
|
||||
else
|
||||
img.img-center.img-responsive.public-profile-img.border-radius-5(src='https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png')
|
||||
h1.text-center.negative-5.profile-social-icons
|
||||
if (twitter)
|
||||
a.fa.fa-twitter-square.text-primary(title="@#{username}'s Twitter Profile", href='https://twitter.com/' + twitter, target='_blank')
|
||||
if (github)
|
||||
a.fa.fa-github-square.text-primary(title="@#{username}'s GitHub Profile", href=github, target='_blank')
|
||||
if (linkedin)
|
||||
a.fa.fa-linkedin-square.text-primary(title="@#{username}'s LinkedIn Profile", href=linkedin, target='_blank')
|
||||
h1.flat-top.wrappable= name
|
||||
h1.flat-top.wrappable= location
|
||||
p.flat-top.bio= bio
|
||||
h1.flat-top.text-primary= "[ " + (progressTimestamps.length) + " ]"
|
||||
if pledge
|
||||
.spacer
|
||||
h4
|
||||
| This camper has committed to giving $#{pledge.amount} to
|
||||
a(href='#{pledge.donateUrl}?ref=freecodecamp.org' target='_blank') #{pledge.displayName}
|
||||
| each month until they have completed their #{pledge.goal}.
|
||||
.spacer
|
||||
.row
|
||||
.col-xs-12.col-sm-10.col-sm-offset-1.col-md-8.col-md-offset-2
|
||||
if isFrontEndCert
|
||||
a.btn.btn-primary.btn-block(href='/' + username + '/front-end-certification') View My Front End Development Certification
|
||||
if isDataVisCert
|
||||
.button-spacer
|
||||
a.btn.btn-primary.btn-block(href='/' + username + '/data-visualization-certification') View My Data Visualization Certification
|
||||
if isBackEndCert
|
||||
.button-spacer
|
||||
a.btn.btn-primary.btn-block(href='/' + username + '/back-end-certification') View My Back End Development Certification
|
||||
if isRespWebDesignCert
|
||||
.button-spacer
|
||||
a.btn.btn-primary.btn-block(href='/' + username + '/responsive-web-design-certification') View My Responsive Web Design Certification
|
||||
if isFrontEndLibsCert
|
||||
.button-spacer
|
||||
a.btn.btn-primary.btn-block(href='/' + username + '/front-end-libraries-certification') View My Front End Libraries Certification
|
||||
if isJsAlgoDataStructCert
|
||||
.button-spacer
|
||||
a.btn.btn-primary.btn-block(href='/' + username + '/javascript-algorithms-data-structures-certification') View My JavaScript Algorithms Data Structures Certification
|
||||
if isApisMicroservicesCert
|
||||
.button-spacer
|
||||
a.btn.btn-primary.btn-block(href='/' + username + '/apis-microservices-certification') View My APIs Microservices Certification
|
||||
if isInfosecQaCert
|
||||
.button-spacer
|
||||
a.btn.btn-primary.btn-block(href='/' + username + '/information-security-quality-assurance-certification') View My Information Sequrity Quality Assurance Certification
|
||||
if (user && user.username != username)
|
||||
.button-spacer
|
||||
a.btn.btn-primary.btn-block(href='/' + username + '/report-user/') Report this user's profile for abuse
|
||||
.row
|
||||
.col-xs-12.text-center
|
||||
if (badges.coreTeam && badges.coreTeam.length)
|
||||
h4 Core Team #{badges.coreTeam.reverse().join(', ')}
|
||||
|
||||
.spacer
|
||||
.col-md-12
|
||||
#cal-heatmap.hidden-xs.hidden-sm.d3-centered
|
||||
script.
|
||||
$(document).ready(function () {
|
||||
var cal = new CalHeatMap();
|
||||
var calendar = !{JSON.stringify(calender)};
|
||||
var rectSelector = "#cal-heatmap > svg > svg.graph-legend > g > rect.r";
|
||||
var calLegendTitles = ["0 items", "1 item", "2 items", "3 or more items"]
|
||||
cal.init({
|
||||
itemSelector: "#cal-heatmap",
|
||||
domain: "month",
|
||||
subDomain: "x_day",
|
||||
domainGutter: 10,
|
||||
data: calendar,
|
||||
cellSize: 15,
|
||||
align: 'center',
|
||||
cellRadius: 3,
|
||||
cellPadding: 2,
|
||||
tooltip: true,
|
||||
range: 6,
|
||||
start: new Date().setDate(new Date().getDate() - 150),
|
||||
legendColors: ["#cccccc", "#215f1e"],
|
||||
legend: [1, 2, 3],
|
||||
label: {
|
||||
position: "top"
|
||||
}
|
||||
});
|
||||
calLegendTitles.forEach(function(title, i) {
|
||||
document.querySelector(rectSelector + (i + 1).toString() + '> title').innerHTML = title;
|
||||
});
|
||||
});
|
||||
.row
|
||||
.hidden-xs.col-sm-12.text-center
|
||||
.row.text-primary
|
||||
h4.col-sm-6.text-right Longest Streak: #{longestStreak} #{longestStreak === 1 ? ' day' : ' days'}
|
||||
h4.col-sm-6.text-left Current Streak: #{currentStreak} #{currentStreak === 1 ? ' day' : ' days'}
|
||||
|
||||
|
||||
if (user && user.username == username || !isLocked)
|
||||
if (projects .length > 0)
|
||||
.col-sm-12
|
||||
table.table.table-striped
|
||||
thead
|
||||
tr
|
||||
th.col-xs-5 Projects
|
||||
th.col-xs-2.hidden-xs Completed
|
||||
th.col-xs-2.hidden-xs Last Updated
|
||||
th.col-xs-2.hidden-xs Link
|
||||
for challenge in projects
|
||||
tr
|
||||
td.col-xs-5.hidden-xs
|
||||
a(href='/challenges/#{challenge.block}/#{challenge.dashedName}', target='_blank')= challenge.name
|
||||
td.col-xs-2.hidden-xs= challenge.completedDate ? challenge.completedDate : 'Not Available'
|
||||
td.col-xs-2.hidden-xs= challenge.lastUpdated ? challenge.lastUpdated : ''
|
||||
td.col-xs-2.hidden-xs
|
||||
a(href=challenge.solution, target='_blank') View project
|
||||
td.col-xs-12.visible-xs
|
||||
a(href=challenge.solution, target='_blank')= challenge.name
|
||||
if (algorithms.length > 0)
|
||||
.col-sm-12
|
||||
table.table.table-striped
|
||||
thead
|
||||
tr
|
||||
th.col-xs-5 Algorithms
|
||||
th.col-xs-2.hidden-xs Completed
|
||||
th.col-xs-2.hidden-xs Last Updated
|
||||
th.col-xs-2.hidden-xs Solution
|
||||
for challenge in algorithms
|
||||
tr
|
||||
td.col-xs-5.hidden-xs= challenge.name
|
||||
td.col-xs-2.hidden-xs= challenge.completedDate ? challenge.completedDate : 'Not Available'
|
||||
td.col-xs-2.hidden-xs= challenge.lastUpdated ? challenge.lastUpdated : ''
|
||||
td.col-xs-2.hidden-xs
|
||||
if (challenge.solution)
|
||||
a(href='/challenges/#{challenge.block}/#{challenge.dashedName}?solution=#{encodeURIComponent(encodeFcc(challenge.solution))}', target='_blank') View solution
|
||||
else
|
||||
a(href='/challenges/#{challenge.block}/#{challenge.dashedName}') View this challenge
|
||||
td.col-xs-12.visible-xs
|
||||
if (challenge.solution)
|
||||
a(href='/challenges/#{challenge.block}/#{challenge.dashedName}?solution=#{encodeURIComponent(encodeFcc(challenge.solution))}', target='_blank')= challenge.name
|
||||
else
|
||||
a(href='/challenges/#{challenge.block}/#{challenge.dashedName}')= challenge.name
|
||||
if (challenges.length > 0)
|
||||
.col-sm-12
|
||||
table.table.table-striped
|
||||
thead
|
||||
tr
|
||||
th.col-xs-5 Challenges
|
||||
th.col-xs-2.hidden-xs Completed
|
||||
th.col-xs-2.hidden-xs Last Updated
|
||||
th.col-xs-2.hidden-xs Solution
|
||||
for challenge in challenges
|
||||
tr
|
||||
td.col-xs-5.hidden-xs= challenge.name
|
||||
td.col-xs-2.hidden-xs= challenge.completedDate ? challenge.completedDate : 'Not Available'
|
||||
td.col-xs-2.hidden-xs= challenge.lastUpdated ? challenge.lastUpdated : ''
|
||||
td.col-xs-2.hidden-xs
|
||||
if (challenge.solution && challenge.name)
|
||||
a(href='/challenges/#{challenge.block}/#{challenge.dashedName}?solution=#{encodeURIComponent(encodeFcc(challenge.solution))}', target='_blank') View solution
|
||||
else if (challenge.name)
|
||||
a(href='/challenges/#{challenge.block}/#{challenge.dashedName}') View this challenge
|
||||
else
|
||||
span N/A
|
||||
td.col-xs-12.visible-xs
|
||||
if (challenge.solution && challenge.name)
|
||||
a(href='/challenges/#{challenge.block}/#{challenge.dashedName}?solution=#{encodeURIComponent(encodeFcc(challenge.solution))}', target='_blank')= challenge.name
|
||||
else if (challenge.name)
|
||||
a(href='/challenges/#{challenge.block}/#{challenge.dashedName}')= challenge.name
|
||||
else
|
||||
span N/A
|
57
api-server/server/views/account/update-email.jade
Normal file
57
api-server/server/views/account/update-email.jade
Normal file
@ -0,0 +1,57 @@
|
||||
extends ../layout
|
||||
block content
|
||||
.row.flashMessage.negative-30
|
||||
.col-xs-12.col-sm-8.col-sm-offset-2.col-md-6.col-md-offset-3
|
||||
#flash-board.alert.fade.in(style='display: none;')
|
||||
button.close(type='button', data-dismiss='alert')
|
||||
i.fas.fa-times-circle#flash-close
|
||||
#flash-content
|
||||
.container
|
||||
h2.text-center Update your email address here:
|
||||
form.form-horizontal.update-email(method='POST', action='/update-my-email', name="updateEmailForm")
|
||||
.row
|
||||
.col-sm-6.col-sm-offset-3
|
||||
input(type='hidden', name='_csrf', value=_csrf)
|
||||
.form-group
|
||||
input.input-lg.form-control(type='email', name='email', id='email', value=user.email || '', placeholder=user.email || 'Enter your new email', autofocus, required, autocomplete="off")
|
||||
.form-group
|
||||
button.btn.btn-lg.btn-primary.btn-block(type='submit')= !user.email || user.emailVerified ? 'Update my Email' : 'Verify Email'
|
||||
a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='/signout')
|
||||
| Sign out
|
||||
|
||||
script.
|
||||
$(document).ready(function() {
|
||||
$('form').submit(function(event){
|
||||
event.preventDefault();
|
||||
$('#flash-board').hide();
|
||||
var $form = $(event.target);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : $form.attr('action'),
|
||||
data : $form.serialize(),
|
||||
dataType : 'json',
|
||||
encode : true,
|
||||
xhrFields : { withCredentials: true }
|
||||
})
|
||||
.fail(error => {
|
||||
if (error.responseText){
|
||||
var data = JSON.parse(error.responseText);
|
||||
if(data.message)
|
||||
$('#flash-content').html(data.message);
|
||||
$('#flash-board')
|
||||
.removeClass('alert-success')
|
||||
.addClass('alert-info')
|
||||
.fadeIn();
|
||||
}
|
||||
})
|
||||
.done(data =>{
|
||||
if(data && data.message){
|
||||
$('#flash-content').html(data.message);
|
||||
$('#flash-board')
|
||||
.removeClass('alert-info')
|
||||
.addClass('alert-success')
|
||||
.fadeIn();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
32
api-server/server/views/certificate/advanced-front-end.jade
Normal file
32
api-server/server/views/certificate/advanced-front-end.jade
Normal file
@ -0,0 +1,32 @@
|
||||
meta(name='viewport', content='width=device-width, initial-scale=1')
|
||||
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css')
|
||||
include styles
|
||||
|
||||
.certificate-wrapper.container
|
||||
.row
|
||||
header
|
||||
.col-md-5.col-sm-12
|
||||
.logo
|
||||
img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo")
|
||||
.col-md-7.col-sm-12
|
||||
.issue-date Issued
|
||||
strong #{date}
|
||||
|
||||
section.information
|
||||
.information-container
|
||||
h3 This certifies that
|
||||
h1
|
||||
strong= name
|
||||
h3 has successfully completed freeCodeCamp's
|
||||
h1
|
||||
strong Advanced Frontend
|
||||
h4 1 of 3 legacy freeCodeCamp certification, representing approximately 400 hours of coursework
|
||||
|
||||
footer
|
||||
.row.signatures
|
||||
img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature")
|
||||
p
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/advanced-front-end
|
@ -0,0 +1,32 @@
|
||||
meta(name='viewport', content='width=device-width, initial-scale=1')
|
||||
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css')
|
||||
include styles
|
||||
|
||||
.certificate-wrapper.container
|
||||
.row
|
||||
header
|
||||
.col-md-5.col-sm-12
|
||||
.logo
|
||||
img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo")
|
||||
.col-md-7.col-sm-12
|
||||
.issue-date Issued
|
||||
strong #{date}
|
||||
|
||||
section.information
|
||||
.information-container
|
||||
h3 This certifies that
|
||||
h1
|
||||
strong= name
|
||||
h3 has successfully completed freeCodeCamp's
|
||||
h1
|
||||
strong APIs and Microservices
|
||||
h4 Developer Certification, representing approximately 300 hours of coursework
|
||||
|
||||
footer
|
||||
.row.signatures
|
||||
img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature")
|
||||
p
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/apis-and-microservices
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user