Files
freeCodeCamp/api-server/server/boot/certificate.js

545 lines
15 KiB
JavaScript

import _ from 'lodash';
import loopback from 'loopback';
import path from 'path';
import dedent from 'dedent';
import { Observable } from 'rx';
import debug from 'debug';
import { isEmail } from 'validator';
import { reportError } from '../middlewares/sentry-error-handler.js';
import { ifNoUser401 } from '../utils/middleware';
import { observeQuery } from '../utils/rx';
import {
legacyFrontEndChallengeId,
legacyBackEndChallengeId,
legacyDataVisId,
legacyInfosecQaId,
legacyFullStackId,
respWebDesignId,
frontEndLibsId,
jsAlgoDataStructId,
dataVis2018Id,
apisMicroservicesId,
qaV7Id,
infosecV7Id,
sciCompPyV7Id,
dataAnalysisPyV7Id,
machineLearningPyV7Id
} from '../utils/constantStrings.json';
import { oldDataVizId } from '../../../config/misc';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
import { getChallenges } from '../utils/get-curriculum';
const log = debug('fcc:certification');
export default function bootCertificate(app, done) {
const api = app.loopback.Router();
// TODO: rather than getting all the challenges, then grabbing the certs,
// consider just getting the certs.
getChallenges().then(allChallenges => {
const certTypeIds = createCertTypeIds(allChallenges);
const showCert = createShowCert(app);
const verifyCert = createVerifyCert(certTypeIds, app);
api.put('/certificate/verify', ifNoUser401, ifNoSuperBlock404, verifyCert);
api.get('/certificate/showCert/:username/:cert', showCert);
app.use(api);
done();
});
}
export function getFallbackFrontEndDate(completedChallenges, completedDate) {
var chalIds = [...Object.values(certIds), oldDataVizId];
const latestCertDate = completedChallenges
.filter(chal => chalIds.includes(chal.id))
.sort((a, b) => b.completedDate - a.completedDate)[0].completedDate;
return latestCertDate ? latestCertDate : completedDate;
}
function ifNoSuperBlock404(req, res, next) {
const { superBlock } = req.body;
if (superBlock && superBlocks.includes(superBlock)) {
return next();
}
return res.status(404).end();
}
const renderCertifiedEmail = loopback.template(
path.join(__dirname, '..', 'views', 'emails', 'certified.ejs')
);
function createCertTypeIds(allChallenges) {
return {
// legacy
[certTypes.frontEnd]: getCertById(legacyFrontEndChallengeId, allChallenges),
[certTypes.backEnd]: getCertById(legacyBackEndChallengeId, allChallenges),
[certTypes.dataVis]: getCertById(legacyDataVisId, allChallenges),
[certTypes.infosecQa]: getCertById(legacyInfosecQaId, allChallenges),
[certTypes.fullStack]: getCertById(legacyFullStackId, allChallenges),
// modern
[certTypes.respWebDesign]: getCertById(respWebDesignId, allChallenges),
[certTypes.frontEndLibs]: getCertById(frontEndLibsId, allChallenges),
[certTypes.dataVis2018]: getCertById(dataVis2018Id, allChallenges),
[certTypes.jsAlgoDataStruct]: getCertById(
jsAlgoDataStructId,
allChallenges
),
[certTypes.apisMicroservices]: getCertById(
apisMicroservicesId,
allChallenges
),
[certTypes.qaV7]: getCertById(qaV7Id, allChallenges),
[certTypes.infosecV7]: getCertById(infosecV7Id, allChallenges),
[certTypes.sciCompPyV7]: getCertById(sciCompPyV7Id, allChallenges),
[certTypes.dataAnalysisPyV7]: getCertById(
dataAnalysisPyV7Id,
allChallenges
),
[certTypes.machineLearningPyV7]: getCertById(
machineLearningPyV7Id,
allChallenges
)
};
}
function canClaim(ids, completedChallenges = []) {
return _.every(ids, ({ id }) =>
_.find(completedChallenges, ({ id: completedId }) => completedId === id)
);
}
const certIds = {
[certTypes.frontEnd]: legacyFrontEndChallengeId,
[certTypes.backEnd]: legacyBackEndChallengeId,
[certTypes.dataVis]: legacyDataVisId,
[certTypes.infosecQa]: legacyInfosecQaId,
[certTypes.fullStack]: legacyFullStackId,
[certTypes.respWebDesign]: respWebDesignId,
[certTypes.frontEndLibs]: frontEndLibsId,
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
[certTypes.dataVis2018]: dataVis2018Id,
[certTypes.apisMicroservices]: apisMicroservicesId,
[certTypes.qaV7]: qaV7Id,
[certTypes.infosecV7]: infosecV7Id,
[certTypes.sciCompPyV7]: sciCompPyV7Id,
[certTypes.dataAnalysisPyV7]: dataAnalysisPyV7Id,
[certTypes.machineLearningPyV7]: machineLearningPyV7Id
};
const certText = {
[certTypes.frontEnd]: 'Legacy Front End',
[certTypes.backEnd]: 'Legacy Back End',
[certTypes.dataVis]: 'Legacy Data Visualization',
[certTypes.infosecQa]: 'Legacy Information Security and Quality Assurance',
[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.qaV7]: 'Quality Assurance',
[certTypes.infosecV7]: 'Information Security',
[certTypes.sciCompPyV7]: 'Scientific Computing with Python',
[certTypes.dataAnalysisPyV7]: 'Data Analysis with Python',
[certTypes.machineLearningPyV7]: 'Machine Learning with Python'
};
const completionHours = {
[certTypes.frontEnd]: 400,
[certTypes.backEnd]: 400,
[certTypes.dataVis]: 400,
[certTypes.infosecQa]: 300,
[certTypes.fullStack]: 1800,
[certTypes.respWebDesign]: 300,
[certTypes.frontEndLibs]: 300,
[certTypes.jsAlgoDataStruct]: 300,
[certTypes.dataVis2018]: 300,
[certTypes.apisMicroservices]: 300,
[certTypes.qaV7]: 300,
[certTypes.infosecV7]: 300,
[certTypes.sciCompPyV7]: 300,
[certTypes.dataAnalysisPyV7]: 300,
[certTypes.machineLearningPyV7]: 300
};
function getCertById(anId, allChallenges) {
return allChallenges
.filter(({ id }) => id === anId)
.map(({ id, tests, name, challengeType }) => ({
id,
tests,
name,
challengeType
}))[0];
}
const superBlocks = Object.keys(superBlockCertTypeMap);
function sendCertifiedEmail(
{
email = '',
name,
username,
isRespWebDesignCert,
isFrontEndLibsCert,
isJsAlgoDataStructCert,
isDataVisCert,
isApisMicroservicesCert,
isQaCertV7,
isInfosecCertV7,
isSciCompPyCertV7,
isDataAnalysisPyCertV7,
isMachineLearningPyCertV7
},
send$
) {
if (
!isEmail(email) ||
!isRespWebDesignCert ||
!isFrontEndLibsCert ||
!isJsAlgoDataStructCert ||
!isDataVisCert ||
!isApisMicroservicesCert ||
!isQaCertV7 ||
!isInfosecCertV7 ||
!isSciCompPyCertV7 ||
!isDataAnalysisPyCertV7 ||
!isMachineLearningPyCertV7
) {
return Observable.just(false);
}
const notifyUser = {
type: 'email',
to: email,
from: 'quincy@freecodecamp.org',
subject: dedent`
Congratulations on completing all of the
freeCodeCamp certifications!
`,
text: renderCertifiedEmail({
username,
name
})
};
return send$(notifyUser).map(() => true);
}
function getUserIsCertMap(user) {
const {
isRespWebDesignCert = false,
isJsAlgoDataStructCert = false,
isFrontEndLibsCert = false,
is2018DataVisCert = false,
isApisMicroservicesCert = false,
isInfosecQaCert = false,
isQaCertV7 = false,
isInfosecCertV7 = false,
isFrontEndCert = false,
isBackEndCert = false,
isDataVisCert = false,
isFullStackCert = false,
isSciCompPyCertV7 = false,
isDataAnalysisPyCertV7 = false,
isMachineLearningPyCertV7 = false
} = user;
return {
isRespWebDesignCert,
isJsAlgoDataStructCert,
isFrontEndLibsCert,
is2018DataVisCert,
isApisMicroservicesCert,
isInfosecQaCert,
isQaCertV7,
isInfosecCertV7,
isFrontEndCert,
isBackEndCert,
isDataVisCert,
isFullStackCert,
isSciCompPyCertV7,
isDataAnalysisPyCertV7,
isMachineLearningPyCertV7
};
}
function createVerifyCert(certTypeIds, app) {
const { Email } = app.models;
return function verifyCert(req, res, next) {
const {
body: { superBlock },
user
} = req;
log(superBlock);
let certType = superBlockCertTypeMap[superBlock];
log(certType);
return Observable.of(certTypeIds[certType])
.flatMap(challenge => {
const certName = certText[certType];
if (user[certType]) {
return Observable.just({
type: 'info',
message: 'flash.already-claimed',
variables: { name: certName }
});
}
// certificate doesn't exist or
// connection error
if (!challenge) {
reportError(`Error claiming ${certName}`);
return Observable.just({
type: 'danger',
message: 'flash.wrong-name',
variables: { name: certName }
});
}
const { id, tests, challengeType } = challenge;
if (!canClaim(tests, user.completedChallenges)) {
return Observable.just({
type: 'info',
message: 'flash.incomplete-steps',
variables: { name: certName }
});
}
const updateData = {
[certType]: true,
completedChallenges: [
...user.completedChallenges,
{
id,
completedDate: new Date(),
challengeType
}
]
};
if (!user.name) {
return Observable.just({
type: 'info',
message: 'flash.name-needed'
});
}
// set here so sendCertifiedEmail works properly
// not used otherwise
user[certType] = true;
const updatePromise = new Promise((resolve, reject) =>
user.updateAttributes(updateData, err => {
if (err) {
return reject(err);
}
return resolve();
})
);
return Observable.combineLatest(
// update user data
Observable.fromPromise(updatePromise),
// sends notification email is user has all 6 certs
// if not it noop
sendCertifiedEmail(user, Email.send$),
(_, pledgeOrMessage) => ({ pledgeOrMessage })
).map(({ pledgeOrMessage }) => {
if (typeof pledgeOrMessage === 'string') {
log(pledgeOrMessage);
}
log('Certificates updated');
return {
type: 'success',
message: 'flash.cert-claim-success',
variables: {
username: user.username,
name: certName
}
};
});
})
.subscribe(message => {
return res.status(200).json({
response: message,
isCertMap: getUserIsCertMap(user),
// send back the completed challenges
// NOTE: we could just send back the latest challenge, but this
// ensures the challenges are synced.
completedChallenges: user.completedChallenges
});
}, next);
};
}
function createShowCert(app) {
const { User } = app.models;
function findUserByUsername$(username, fields) {
return observeQuery(User, 'findOne', {
where: { username },
fields
});
}
return function showCert(req, res, next) {
let { username, cert } = req.params;
username = username.toLowerCase();
const certType = superBlockCertTypeMap[cert];
const certId = certIds[certType];
const certTitle = certText[certType];
const completionTime = completionHours[certType] || 300;
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,
isQaCertV7: true,
isInfosecCertV7: true,
isSciCompPyCertV7: true,
isDataAnalysisPyCertV7: true,
isMachineLearningPyCertV7: true,
isHonest: true,
username: true,
name: true,
completedChallenges: true,
profileUI: true
}).subscribe(user => {
if (!user) {
return res.json({
messages: [
{
type: 'info',
message: 'flash.username-not-found',
variables: { username: username }
}
]
});
}
const { isLocked, showCerts, showName } = user.profileUI;
if (!user.name) {
return res.json({
messages: [
{
type: 'info',
message: 'flash.add-name'
}
]
});
}
if (user.isCheater) {
return res.json({
messages: [
{
type: 'info',
message: 'flash.not-eligible'
}
]
});
}
if (isLocked) {
return res.json({
messages: [
{
type: 'info',
message: 'flash.profile-private',
variables: { username: username }
}
]
});
}
if (!showCerts) {
return res.json({
messages: [
{
type: 'info',
message: 'flash.certs-private',
variables: { username: username }
}
]
});
}
if (!user.isHonest) {
return res.json({
messages: [
{
type: 'info',
message: 'flash.not-honest',
variables: { username: username }
}
]
});
}
if (user[certType]) {
const { completedChallenges = [] } = user;
const certChallenge = _.find(
completedChallenges,
({ id }) => certId === id
);
let { completedDate = new Date() } = certChallenge || {};
// the challenge id has been rotated for isDataVisCert
if (certType === 'isDataVisCert' && !certChallenge) {
let oldDataVisIdChall = _.find(
completedChallenges,
({ id }) => oldDataVizId === id
);
if (oldDataVisIdChall) {
completedDate = oldDataVisIdChall.completedDate || completedDate;
}
}
// if fullcert is not found, return the latest completedDate
if (certType === 'isFullStackCert' && !certChallenge) {
completedDate = getFallbackFrontEndDate(
completedChallenges,
completedDate
);
}
const { username, name } = user;
if (!showName) {
return res.json({
certTitle,
username,
date: completedDate,
completionTime
});
}
return res.json({
certTitle,
username,
name,
date: completedDate,
completionTime
});
}
return res.json({
messages: [
{
type: 'info',
message: 'flash.user-not-certified',
variables: { username: username, cert: certText[certType] }
}
]
});
}, next);
};
}