feat(api): decouple api from curriculum (#40703)
This commit is contained in:
committed by
GitHub
parent
f4bbe3f34c
commit
c077ffe4b9
@@ -1,544 +0,0 @@
|
||||
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);
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user