Files
freeCodeCamp/server/boot/certificate.js
Berkeley Martinez dbecdc5618 feat: prep for modern challenges (#15781)
* 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
2017-11-09 19:10:30 -06:00

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
);
}
}