2015-10-02 11:47:36 -07:00
|
|
|
import _ from 'lodash';
|
2016-05-03 00:45:34 -07:00
|
|
|
import loopback from 'loopback';
|
2018-02-27 14:03:06 +00:00
|
|
|
import moment from 'moment-timezone';
|
2016-05-03 00:45:34 -07:00
|
|
|
import path from 'path';
|
2015-10-02 11:47:36 -07:00
|
|
|
import dedent from 'dedent';
|
|
|
|
import { Observable } from 'rx';
|
2016-02-09 20:54:49 -08:00
|
|
|
import debug from 'debug';
|
2017-08-26 00:27:05 +02:00
|
|
|
import { isEmail } from 'validator';
|
2015-10-02 11:47:36 -07:00
|
|
|
|
|
|
|
import {
|
2018-02-16 23:18:53 +00:00
|
|
|
ifNoUser401
|
2015-10-02 11:47:36 -07:00
|
|
|
} from '../utils/middleware';
|
|
|
|
|
2016-02-09 20:54:49 -08:00
|
|
|
import { observeQuery } from '../utils/rx';
|
2015-10-02 11:47:36 -07:00
|
|
|
|
2015-10-05 19:58:44 -07:00
|
|
|
import {
|
2018-02-27 14:03:06 +00:00
|
|
|
legacyFrontEndChallengeId,
|
|
|
|
legacyBackEndChallengeId,
|
|
|
|
legacyDataVisId,
|
2018-02-16 23:18:53 +00:00
|
|
|
|
2017-12-21 01:15:23 +00:00
|
|
|
respWebDesignId,
|
|
|
|
frontEndLibsId,
|
|
|
|
jsAlgoDataStructId,
|
2018-02-27 14:03:06 +00:00
|
|
|
dataVis2018Id,
|
2017-12-21 01:15:23 +00:00
|
|
|
apisMicroservicesId,
|
|
|
|
infosecQaId
|
2015-10-05 19:58:44 -07:00
|
|
|
} from '../utils/constantStrings.json';
|
2018-02-27 14:03:06 +00:00
|
|
|
import certTypes from '../utils/certTypes.json';
|
|
|
|
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
|
2015-10-06 21:08:24 -07:00
|
|
|
import {
|
|
|
|
completeCommitment$
|
|
|
|
} from '../utils/commit';
|
|
|
|
|
2016-01-27 11:34:44 -08:00
|
|
|
const log = debug('fcc:certification');
|
2016-05-03 00:45:34 -07:00
|
|
|
const renderCertifedEmail = loopback.template(path.join(
|
|
|
|
__dirname,
|
|
|
|
'..',
|
|
|
|
'views',
|
|
|
|
'emails',
|
|
|
|
'certified.ejs'
|
|
|
|
));
|
2015-10-02 11:47:36 -07:00
|
|
|
|
2018-05-15 14:56:26 +01:00
|
|
|
function isCertified(ids, completedChallenges = []) {
|
|
|
|
return _.every(
|
|
|
|
ids,
|
|
|
|
({ id }) => _.find(
|
|
|
|
completedChallenges,
|
|
|
|
({ id: completedId }) => completedId === id
|
|
|
|
)
|
|
|
|
);
|
2015-10-02 11:47:36 -07:00
|
|
|
}
|
|
|
|
|
2018-02-27 14:03:06 +00:00
|
|
|
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
|
|
|
|
};
|
|
|
|
|
|
|
|
const certViews = {
|
|
|
|
[certTypes.frontEnd]: 'certificate/legacy/front-end.jade',
|
|
|
|
[certTypes.backEnd]: 'certificate/legacy/back-end.jade',
|
|
|
|
[certTypes.dataVis]: 'certificate/legacy/data-visualization.jade',
|
|
|
|
[certTypes.fullStack]: 'certificate/legacy/full-stack.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'
|
|
|
|
};
|
|
|
|
|
|
|
|
const certText = {
|
2018-05-15 14:56:26 +01:00
|
|
|
[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',
|
2018-02-27 14:03:06 +00:00
|
|
|
[certTypes.jsAlgoDataStruct]:
|
2018-05-15 14:56:26 +01:00
|
|
|
'JavaScript Algorithms and Data Structures',
|
|
|
|
[certTypes.dataVis2018]: 'Data Visualization',
|
|
|
|
[certTypes.apisMicroservices]: 'APIs and Microservices',
|
|
|
|
[certTypes.infosecQa]: 'Information Security and Quality Assurance'
|
2018-02-27 14:03:06 +00:00
|
|
|
};
|
|
|
|
|
2016-01-11 15:58:37 -08:00
|
|
|
function getIdsForCert$(id, Challenge) {
|
|
|
|
return observeQuery(
|
2015-10-02 11:47:36 -07:00
|
|
|
Challenge,
|
|
|
|
'findById',
|
2016-01-11 15:58:37 -08:00
|
|
|
id,
|
2015-10-02 11:47:36 -07:00
|
|
|
{
|
2015-10-05 19:58:44 -07:00
|
|
|
id: true,
|
|
|
|
tests: true,
|
|
|
|
name: true,
|
|
|
|
challengeType: true
|
2015-10-02 11:47:36 -07:00
|
|
|
}
|
|
|
|
)
|
|
|
|
.shareReplay();
|
2016-01-11 15:58:37 -08:00
|
|
|
}
|
2015-10-02 11:47:36 -07:00
|
|
|
|
2016-05-03 00:45:34 -07:00
|
|
|
function sendCertifiedEmail(
|
|
|
|
{
|
|
|
|
email,
|
|
|
|
name,
|
|
|
|
username,
|
2017-12-21 01:15:23 +00:00
|
|
|
isRespWebDesignCert,
|
|
|
|
isFrontEndLibsCert,
|
|
|
|
isJsAlgoDataStructCert,
|
|
|
|
isDataVisCert,
|
|
|
|
isApisMicroservicesCert,
|
|
|
|
isInfosecQaCert
|
2016-05-03 00:45:34 -07:00
|
|
|
},
|
|
|
|
send$
|
|
|
|
) {
|
|
|
|
if (
|
2017-08-26 00:27:05 +02:00
|
|
|
!isEmail(email) ||
|
2017-12-21 01:15:23 +00:00
|
|
|
!isRespWebDesignCert ||
|
|
|
|
!isFrontEndLibsCert ||
|
|
|
|
!isJsAlgoDataStructCert ||
|
|
|
|
!isDataVisCert ||
|
|
|
|
!isApisMicroservicesCert ||
|
|
|
|
!isInfosecQaCert
|
2016-05-03 00:45:34 -07:00
|
|
|
) {
|
|
|
|
return Observable.just(false);
|
|
|
|
}
|
|
|
|
const notifyUser = {
|
|
|
|
type: 'email',
|
|
|
|
to: email,
|
2017-09-08 10:52:40 -07:00
|
|
|
from: 'team@freeCodeCamp.org',
|
|
|
|
subject: dedent`
|
|
|
|
Congratulations on completing all of the
|
|
|
|
freeCodeCamp certificates!
|
|
|
|
`,
|
2016-05-03 00:45:34 -07:00
|
|
|
text: renderCertifedEmail({
|
|
|
|
username,
|
|
|
|
name
|
|
|
|
})
|
|
|
|
};
|
2017-08-26 00:27:05 +02:00
|
|
|
return send$(notifyUser).map(() => true);
|
2016-05-03 00:45:34 -07:00
|
|
|
}
|
|
|
|
|
2016-01-11 15:58:37 -08:00
|
|
|
export default function certificate(app) {
|
|
|
|
const router = app.loopback.Router();
|
2018-02-27 14:03:06 +00:00
|
|
|
const { Email, Challenge, User } = app.models;
|
|
|
|
|
|
|
|
function findUserByUsername$(username, fields) {
|
|
|
|
return observeQuery(
|
|
|
|
User,
|
|
|
|
'findOne',
|
|
|
|
{
|
|
|
|
where: { username },
|
|
|
|
fields
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2016-01-11 15:58:37 -08:00
|
|
|
|
|
|
|
const certTypeIds = {
|
2018-02-16 23:18:53 +00:00
|
|
|
// legacy
|
2018-02-27 14:03:06 +00:00
|
|
|
[certTypes.frontEnd]: getIdsForCert$(legacyFrontEndChallengeId, Challenge),
|
|
|
|
[certTypes.backEnd]: getIdsForCert$(legacyBackEndChallengeId, Challenge),
|
|
|
|
[certTypes.dataVis]: getIdsForCert$(legacyDataVisId, Challenge),
|
2018-02-16 23:18:53 +00:00
|
|
|
|
|
|
|
// modern
|
2017-12-21 01:15:23 +00:00
|
|
|
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
|
|
|
|
[certTypes.frontEndLibs]: getIdsForCert$(frontEndLibsId, Challenge),
|
2018-02-16 23:18:53 +00:00
|
|
|
[certTypes.dataVis2018]: getIdsForCert$(dataVis2018Id, Challenge),
|
2017-12-21 01:15:23 +00:00
|
|
|
[certTypes.jsAlgoDataStruct]: getIdsForCert$(jsAlgoDataStructId, Challenge),
|
|
|
|
[certTypes.apisMicroservices]: getIdsForCert$(
|
|
|
|
apisMicroservicesId,
|
|
|
|
Challenge
|
|
|
|
),
|
|
|
|
[certTypes.infosecQa]: getIdsForCert$(infosecQaId, Challenge)
|
2016-01-11 15:58:37 -08:00
|
|
|
};
|
2015-10-02 11:47:36 -07:00
|
|
|
|
2018-02-16 23:18:53 +00:00
|
|
|
const superBlocks = Object.keys(superBlockCertTypeMap);
|
2017-12-21 01:15:23 +00:00
|
|
|
|
|
|
|
router.post(
|
2018-02-16 23:18:53 +00:00
|
|
|
'/certificate/verify',
|
2017-12-21 01:15:23 +00:00
|
|
|
ifNoUser401,
|
2018-02-16 23:18:53 +00:00
|
|
|
ifNoSuperBlock404,
|
|
|
|
verifyCert
|
2015-10-02 11:47:36 -07:00
|
|
|
);
|
2018-02-27 14:03:06 +00:00
|
|
|
router.get(
|
2018-03-06 10:38:37 +00:00
|
|
|
'/certificates/:username/:cert',
|
2018-02-27 14:03:06 +00:00
|
|
|
showCert
|
|
|
|
);
|
2015-10-02 11:47:36 -07:00
|
|
|
|
|
|
|
app.use(router);
|
|
|
|
|
2018-02-16 23:18:53 +00:00
|
|
|
const noNameMessage = dedent`
|
|
|
|
We need your name so we can put it on your certificate.
|
|
|
|
Add your name to your account settings and click the save button.
|
|
|
|
Then we can issue your certificate.
|
|
|
|
`;
|
|
|
|
|
|
|
|
const notCertifiedMessage = name => dedent`
|
2018-04-25 01:07:27 -04:00
|
|
|
it looks like you have not completed the necessary steps.
|
2018-02-16 23:18:53 +00:00
|
|
|
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`
|
2018-04-25 01:07:27 -04:00
|
|
|
@${username}, you have successfully claimed
|
2018-02-16 23:18:53 +00:00
|
|
|
the ${name}!
|
|
|
|
Congratulations on behalf of the freeCodeCamp team!
|
|
|
|
`;
|
|
|
|
|
|
|
|
function verifyCert(req, res, next) {
|
|
|
|
const { body: { superBlock }, user } = req;
|
2018-02-27 14:03:06 +00:00
|
|
|
log(superBlock);
|
2018-02-16 23:18:53 +00:00
|
|
|
let certType = superBlockCertTypeMap[superBlock];
|
|
|
|
log(certType);
|
2018-05-15 14:56:26 +01:00
|
|
|
return user.getCompletedChallenges$()
|
2018-02-27 14:03:06 +00:00
|
|
|
.flatMap(() => certTypeIds[certType])
|
|
|
|
.flatMap(challenge => {
|
2015-10-05 19:58:44 -07:00
|
|
|
const {
|
|
|
|
id,
|
|
|
|
tests,
|
|
|
|
challengeType
|
|
|
|
} = challenge;
|
2018-05-15 14:56:26 +01:00
|
|
|
const certName = certText[certType];
|
2018-02-16 23:18:53 +00:00
|
|
|
if (user[certType]) {
|
2018-05-15 14:56:26 +01:00
|
|
|
return Observable.just(alreadyClaimedMessage(certName));
|
2018-02-16 23:18:53 +00:00
|
|
|
}
|
2018-05-15 14:56:26 +01:00
|
|
|
if (!user[certType] && !isCertified(tests, user.completedChallenges)) {
|
|
|
|
return Observable.just(notCertifiedMessage(certName));
|
2018-02-16 23:18:53 +00:00
|
|
|
}
|
|
|
|
if (!user.name) {
|
|
|
|
return Observable.just(noNameMessage);
|
2015-10-02 11:47:36 -07:00
|
|
|
}
|
2016-05-03 00:45:34 -07:00
|
|
|
const updateData = {
|
2018-05-15 14:56:26 +01:00
|
|
|
$push: {
|
|
|
|
completedChallenges: {
|
2016-05-03 00:45:34 -07:00
|
|
|
id,
|
|
|
|
completedDate: new Date(),
|
|
|
|
challengeType
|
2018-05-15 14:56:26 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
$set: {
|
2016-05-03 00:45:34 -07:00
|
|
|
[certType]: true
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// set here so sendCertifiedEmail works properly
|
|
|
|
// not used otherwise
|
|
|
|
user[certType] = true;
|
2018-05-15 14:56:26 +01:00
|
|
|
user.completedChallenges[
|
|
|
|
user.completedChallenges.length - 1
|
|
|
|
] = { id, completedDate: new Date() };
|
2016-05-03 00:45:34 -07:00
|
|
|
return Observable.combineLatest(
|
|
|
|
// update user data
|
|
|
|
user.update$(updateData),
|
|
|
|
// If user has committed to nonprofit,
|
|
|
|
// this will complete their pledge
|
|
|
|
completeCommitment$(user),
|
2018-05-15 14:56:26 +01:00
|
|
|
// sends notification email is user has all 6 certs
|
2016-05-03 00:45:34 -07:00
|
|
|
// if not it noop
|
|
|
|
sendCertifiedEmail(user, Email.send$),
|
|
|
|
({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage })
|
|
|
|
)
|
2018-02-16 23:18:53 +00:00
|
|
|
.map(
|
2016-05-03 00:45:34 -07:00
|
|
|
({ count, pledgeOrMessage }) => {
|
|
|
|
if (typeof pledgeOrMessage === 'string') {
|
|
|
|
log(pledgeOrMessage);
|
|
|
|
}
|
|
|
|
log(`${count} documents updated`);
|
2018-05-15 14:56:26 +01:00
|
|
|
return successMessage(user.username, certName);
|
2016-05-03 00:45:34 -07:00
|
|
|
}
|
|
|
|
);
|
2018-02-16 23:18:53 +00:00
|
|
|
})
|
2015-10-02 11:47:36 -07:00
|
|
|
.subscribe(
|
2018-02-16 23:18:53 +00:00
|
|
|
(message) => {
|
|
|
|
return res.status(200).json({
|
|
|
|
message,
|
|
|
|
success: message.includes('Congratulations')
|
|
|
|
});
|
2015-10-02 11:47:36 -07:00
|
|
|
},
|
|
|
|
next
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-02-16 23:18:53 +00:00
|
|
|
function ifNoSuperBlock404(req, res, next) {
|
|
|
|
const { superBlock } = req.body;
|
|
|
|
if (superBlock && superBlocks.includes(superBlock)) {
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
return res.status(404).end();
|
2015-10-02 11:47:36 -07:00
|
|
|
}
|
2018-02-27 14:03:06 +00:00
|
|
|
|
|
|
|
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,
|
2018-05-20 04:07:41 +01:00
|
|
|
completedChallenges: true,
|
|
|
|
profileUI: true
|
2018-02-27 14:03:06 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
.subscribe(
|
|
|
|
user => {
|
2018-05-20 04:07:41 +01:00
|
|
|
const { isLocked, showCerts } = user.profileUI;
|
2018-05-15 14:56:26 +01:00
|
|
|
const profile = `/portfolio/${user.username}`;
|
2018-02-27 14:03:06 +00:00
|
|
|
if (!user) {
|
|
|
|
req.flash(
|
|
|
|
'danger',
|
|
|
|
`We couldn't find a user with the username ${username}`
|
|
|
|
);
|
|
|
|
return res.redirect('/');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!user.name) {
|
|
|
|
req.flash(
|
|
|
|
'danger',
|
|
|
|
dedent`
|
|
|
|
This user needs to add their name to their account
|
2018-05-20 04:07:41 +01:00
|
|
|
in order for others to be able to view their certification.
|
2018-02-27 14:03:06 +00:00
|
|
|
`
|
|
|
|
);
|
|
|
|
return res.redirect(profile);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (user.isCheater) {
|
2018-05-15 14:56:26 +01:00
|
|
|
return res.redirect(profile);
|
2018-02-27 14:03:06 +00:00
|
|
|
}
|
|
|
|
|
2018-05-20 04:07:41 +01:00
|
|
|
if (isLocked) {
|
2018-02-27 14:03:06 +00:00
|
|
|
req.flash(
|
|
|
|
'danger',
|
|
|
|
dedent`
|
|
|
|
${username} has chosen to make their profile
|
|
|
|
private. They will need to make their profile public
|
2018-05-20 04:07:41 +01:00
|
|
|
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.
|
2018-02-27 14:03:06 +00:00
|
|
|
`
|
|
|
|
);
|
|
|
|
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]) {
|
2018-05-15 14:56:26 +01:00
|
|
|
const { completedChallenges = {} } = user;
|
|
|
|
const { completedDate = new Date() } = _.find(
|
|
|
|
completedChallenges, ({ id }) => certId === id
|
|
|
|
) || {};
|
2018-02-27 14:03:06 +00:00
|
|
|
|
|
|
|
return res.render(
|
|
|
|
certViews[certType],
|
|
|
|
{
|
|
|
|
username: user.username,
|
|
|
|
date: moment(new Date(completedDate)).format('MMMM D, YYYY'),
|
|
|
|
name: user.name
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
req.flash(
|
|
|
|
'danger',
|
2018-05-15 14:56:26 +01:00
|
|
|
`Looks like user ${username} is not ${certText[certType]} certified`
|
2018-02-27 14:03:06 +00:00
|
|
|
);
|
|
|
|
return res.redirect(profile);
|
|
|
|
},
|
|
|
|
next
|
|
|
|
);
|
|
|
|
}
|
2015-10-02 11:47:36 -07:00
|
|
|
}
|