freeCodeCamp/server/boot/certificate.js

414 lines
11 KiB
JavaScript
Raw Normal View History

2015-10-02 11:47:36 -07:00
import _ from 'lodash';
import loopback from 'loopback';
import moment from 'moment-timezone';
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 {
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
import {
legacyFrontEndChallengeId,
legacyBackEndChallengeId,
legacyDataVisId,
respWebDesignId,
frontEndLibsId,
jsAlgoDataStructId,
dataVis2018Id,
apisMicroservicesId,
infosecQaId
} from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
import {
completeCommitment$
} from '../utils/commit';
2016-01-27 11:34:44 -08:00
const log = debug('fcc:certification');
const renderCertifedEmail = loopback.template(path.join(
__dirname,
'..',
'views',
'emails',
'certified.ejs'
));
2015-10-02 11:47:36 -07:00
function isCertified(ids, completedChallenges = []) {
return _.every(
ids,
({ id }) => _.find(
completedChallenges,
({ id: completedId }) => completedId === id
)
);
2015-10-02 11:47:36 -07: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 = {
[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'
};
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
{
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
function sendCertifiedEmail(
{
email,
name,
username,
isRespWebDesignCert,
isFrontEndLibsCert,
isJsAlgoDataStructCert,
isDataVisCert,
isApisMicroservicesCert,
isInfosecQaCert
},
send$
) {
if (
2017-08-26 00:27:05 +02:00
!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 certificates!
`,
text: renderCertifedEmail({
username,
name
})
};
2017-08-26 00:27:05 +02:00
return send$(notifyUser).map(() => true);
}
2016-01-11 15:58:37 -08:00
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
}
);
}
2016-01-11 15:58:37 -08:00
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)
2016-01-11 15:58:37 -08:00
};
2015-10-02 11:47:36 -07:00
const superBlocks = Object.keys(superBlockCertTypeMap);
router.post(
'/certificate/verify',
ifNoUser401,
ifNoSuperBlock404,
verifyCert
2015-10-02 11:47:36 -07:00
);
router.get(
'/certificates/:username/:cert',
showCert
);
2015-10-02 11:47:36 -07:00
app.use(router);
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`
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 {
id,
tests,
challengeType
} = challenge;
const certName = certText[certType];
if (user[certType]) {
return Observable.just(alreadyClaimedMessage(certName));
}
if (!user[certType] && !isCertified(tests, user.completedChallenges)) {
return Observable.just(notCertifiedMessage(certName));
}
if (!user.name) {
return Observable.just(noNameMessage);
2015-10-02 11:47:36 -07:00
}
const updateData = {
$push: {
completedChallenges: {
id,
completedDate: new Date(),
challengeType
}
},
$set: {
[certType]: true
}
};
// set here so sendCertifiedEmail works properly
// not used otherwise
user[certType] = true;
user.completedChallenges[
user.completedChallenges.length - 1
] = { id, completedDate: new Date() };
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);
}
);
})
2015-10-02 11:47:36 -07:00
.subscribe(
(message) => {
return res.status(200).json({
message,
success: message.includes('Congratulations')
});
2015-10-02 11:47:36 -07:00
},
next
);
}
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
}
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 => {
const { isLocked, showCerts } = user.profileUI;
const profile = `/portfolio/${user.username}`;
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
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
);
}
2015-10-02 11:47:36 -07:00
}