* feat(seed): Add modern challenge * chore(react): Use prop-types package * feat: Initial refactor to redux-first-router BREAKING CHANGE: Everything is different! * feat: First rendering * feat(routes): Challenges view render but failing * fix(Challenges): Remove contain HOC * fix(RFR): Add params selector * fix(RFR): :en should be :lang * fix: Update berks utils for redux * fix(Map): Challenge link to arg * fix(Map): Add trailing slash to map page * fix(RFR): Use FCC Link Use fcc Link to get around issue of lang being undefined * fix(Router): Link to is required * fix(app): Rely on RFR state for app lang * chore(RFR): Remove unused RFR Link * fix(RFR): Hydrate initial challenge using RFR and RO * fix: Casing issue * fix(RFR): Undefined links * fix(RFR): Use onRoute<name> convention for route types * feat(server/react): Add helpful redux logging/throwing * fix(server/react): Strip out nonjson from state This prevents thunks in routesMap from breaking serialization * fix(RFR/Link): Should accept any renderable * fix(RFR): Get redirects working * fix(RFR): Redirects and not found's * fix(Map): Move challenge onClick handler * fix(Map): Allow Router.link to handle clicks after onClick * fix(routes): Remove react-router-redux * feat(Router): Add lang to all route actions by default * fix(entities): Only fetch challenge if not already loaded * fix(Files): Move files to own feature * chore(Challenges): Remove vestigial hints logic * fix(RFR): Update challenges on route challenges * fix(code-storage): Should use events instead of commands * fix(Map): ClickOnMap should not hold on to event * chore(lint): Use eslint-config-freecodecamp Closes #15938 * feat(Panes): Update panes on route instead of render * fix(Panes): Store panesmap and update on fetchchallenges * fix(Panes): Normalize panesmaps * fix(Panes): Remove filter from createpanemap * fix(Panes): Middleware on location meta object * feat(Panes): Filter preview on nonhtml challenges * build(babel): Add lodash babel plugin * chore(lint): Lint js files * fix(server/user-stats): Remove use of lodash chain this interferes with babel-plugin-lodash * feat(dev): Add remote redux devtools for ssr * fix(Panes): Dispatch mount action this is needed to trigger window/divider epics * fix(Panes): Getpane to use new panesmap format * fix(Panes): Always update panes after state this lets the panes logic be affected by changes in state
		
			
				
	
	
		
			229 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			229 lines
		
	
	
		
			5.5 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 {
 | 
						|
  ifNoUser401,
 | 
						|
  ifNoUserSend
 | 
						|
} from '../utils/middleware';
 | 
						|
 | 
						|
import { observeQuery } from '../utils/rx';
 | 
						|
 | 
						|
import {
 | 
						|
  frontEndChallengeId,
 | 
						|
  dataVisChallengeId,
 | 
						|
  backEndChallengeId
 | 
						|
} 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,
 | 
						|
//     isFrontEndCert: Boolean,
 | 
						|
//     isBackEndCert: Boolean,
 | 
						|
//     isDataVisCert: Boolean
 | 
						|
//   },
 | 
						|
//   send$: Observable
 | 
						|
// ) => Observable
 | 
						|
function sendCertifiedEmail(
 | 
						|
  {
 | 
						|
    email,
 | 
						|
    name,
 | 
						|
    username,
 | 
						|
    isFrontEndCert,
 | 
						|
    isBackEndCert,
 | 
						|
    isDataVisCert
 | 
						|
  },
 | 
						|
  send$
 | 
						|
) {
 | 
						|
  if (
 | 
						|
    !isEmail(email) ||
 | 
						|
    !isFrontEndCert ||
 | 
						|
    !isBackEndCert ||
 | 
						|
    !isDataVisCert
 | 
						|
  ) {
 | 
						|
    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.dataVis]: getIdsForCert$(dataVisChallengeId, Challenge),
 | 
						|
    [certTypes.backEnd]: getIdsForCert$(backEndChallengeId, 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/data-visualization',
 | 
						|
    ifNoUser401,
 | 
						|
    verifyCert.bind(null, certTypes.dataVis)
 | 
						|
  );
 | 
						|
 | 
						|
  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.
 | 
						|
                  <a href="https://github.com/settings/profile">Add your
 | 
						|
                  name to your GitHub account</a>, then go to your
 | 
						|
                  <a href="https://www.freecodecamp.org/settings">settings
 | 
						|
                  page</a> 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
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |