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 { ifNoUser401, ifNoUserSend } from '../utils/middleware'; import { observeQuery } from '../utils/rx'; import { respWebDesignId, frontEndLibsId, jsAlgoDataStructId, frontEndChallengeId, dataVisId, apisMicroservicesId, backEndChallengeId, infosecQaId } from '../utils/constantStrings.json'; import { completeCommitment$ } from '../utils/commit'; import certTypes from '../utils/certTypes.json'; const log = debug('fcc:certification'); const renderCertifedEmail = loopback.template(path.join( __dirname, '..', 'views', 'emails', 'certified.ejs' )); const sendMessageToNonUser = ifNoUserSend( 'must be logged in to complete.' ); function isCertified(ids, challengeMap = {}) { return _.every(ids, ({ id }) => challengeMap[id]); } function getIdsForCert$(id, Challenge) { return observeQuery( Challenge, 'findById', id, { id: true, tests: true, name: true, challengeType: true } ) .shareReplay(); } // sendCertifiedEmail( // { // email: String, // username: String, // isRespWebDesignCert: Boolean, // isFrontEndLibsCert: Boolean, // isJsAlgoDataStructCert: Boolean, // isDataVisCert: Boolean, // isApisMicroservicesCert: Boolean, // isInfosecQaCert: Boolean // }, // send$: Observable // ) => Observable 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 certificates! `, text: renderCertifedEmail({ username, name }) }; return send$(notifyUser).map(() => true); } export default function certificate(app) { const router = app.loopback.Router(); const { Email, Challenge } = app.models; const certTypeIds = { [certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge), [certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge), [certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge), [certTypes.frontEndLibs]: getIdsForCert$(frontEndLibsId, Challenge), [certTypes.jsAlgoDataStruct]: getIdsForCert$(jsAlgoDataStructId, Challenge), [certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge), [certTypes.apisMicroservices]: getIdsForCert$( apisMicroservicesId, Challenge ), [certTypes.infosecQa]: getIdsForCert$(infosecQaId, Challenge) }; router.post( '/certificate/verify/front-end', ifNoUser401, verifyCert.bind(null, certTypes.frontEnd) ); router.post( '/certificate/verify/back-end', ifNoUser401, verifyCert.bind(null, certTypes.backEnd) ); router.post( '/certificate/verify/responsive-web-design', ifNoUser401, verifyCert.bind(null, certTypes.respWebDesign) ); router.post( '/certificate/verify/front-end-libraries', ifNoUser401, verifyCert.bind(null, certTypes.frontEndLibs) ); router.post( '/certificate/verify/javascript-algorithms-data-structures', ifNoUser401, verifyCert.bind(null, certTypes.jsAlgoDataStruct) ); router.post( '/certificate/verify/data-visualization', ifNoUser401, verifyCert.bind(null, certTypes.dataVis) ); router.post( '/certificate/verify/apis-microservices', ifNoUser401, verifyCert.bind(null, certTypes.apisMicroservices) ); router.post( '/certificate/verify/information-security-quality-assurance', ifNoUser401, verifyCert.bind(null, certTypes.infosecQa) ); router.post( '/certificate/honest', sendMessageToNonUser, postHonest ); app.use(router); function verifyCert(certType, req, res, next) { const { user } = req; return user.getChallengeMap$() .flatMap(() => certTypeIds[certType]) .flatMap(challenge => { const { id, tests, name, challengeType } = challenge; if ( user[certType] || !isCertified(tests, user.challengeMap) ) { return Observable.just(false); } const updateData = { $set: { [`challengeMap.${id}`]: { id, name, completedDate: new Date(), challengeType }, [certType]: true } }; // set here so sendCertifiedEmail works properly // not used otherwise user[certType] = true; user.challengeMap[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 three 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 true; } ); }) .subscribe( (didCertify) => { if (didCertify) { // Check if they have a name set if (user.name === '') { return res.status(200).send( dedent` We need your name so we can put it on your certificate. Add your name to your GitHub account, then go to your settings page and click the "update my portfolio from GitHub" button. Then we can issue your certificate. ` ); } return res.status(200).send(true); } return res.status(200).send( dedent` Looks like you have not completed the neccessary steps. Please return to the challenge map. ` ); }, next ); } function postHonest(req, res, next) { return req.user.update$({ $set: { isHonest: true } }).subscribe( () => res.status(200).send(true), next ); } }