feat(settings): Expand Settings page functionality (#16664)
* fix(layout): Fix Settings layout in firefox * chore(availableForHire): Remove available for hire setting * feat(helpers): Use helper components for Settings layout * fix(map): Fix undefined lang requested * feat(settings): Expand Settings page functionality * chore(pledge): Remove pledge from Settings * fix(about): Adjust AboutSettings layout * fix(portfolio): Improve PortfolioSettings layout * fix(email): Improve EmailSettings layout * fix(settings): Align save buttons with form fields * fix(AHP): Format AHP * fix(DangerZone): Adjust DangerZone layout * fix(projectSettings): Change Button Copy * fix(CertSettings): Fix certificate claim logic * chore(lint): Lint
This commit is contained in:
committed by
Quincy Larson
parent
9f034f4f79
commit
24ef69cf7a
@@ -7,9 +7,11 @@ import { check } from 'express-validator/check';
|
||||
|
||||
import {
|
||||
ifUserRedirectTo,
|
||||
ifNoUserRedirectTo,
|
||||
createValidatorErrorHandler
|
||||
} from '../utils/middleware';
|
||||
import { wrapHandledError } from '../utils/create-handled-error.js';
|
||||
import { homeURL } from '../../common/utils/constantStrings.json';
|
||||
|
||||
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
|
||||
// const debug = debugFactory('fcc:boot:auth');
|
||||
@@ -22,6 +24,7 @@ module.exports = function enableAuthentication(app) {
|
||||
// loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html
|
||||
app.enableAuth();
|
||||
const ifUserRedirect = ifUserRedirectTo();
|
||||
const ifNoUserRedirectHome = ifNoUserRedirectTo(homeURL);
|
||||
const router = app.loopback.Router();
|
||||
const api = app.loopback.Router();
|
||||
const { AuthToken, User } = app.models;
|
||||
@@ -79,7 +82,8 @@ module.exports = function enableAuthentication(app) {
|
||||
const {
|
||||
query: {
|
||||
email: encodedEmail,
|
||||
token: authTokenId
|
||||
token: authTokenId,
|
||||
emailChange
|
||||
} = {}
|
||||
} = req;
|
||||
|
||||
@@ -122,14 +126,16 @@ module.exports = function enableAuthentication(app) {
|
||||
);
|
||||
}
|
||||
if (user.email !== email) {
|
||||
throw wrapHandledError(
|
||||
new Error('user email does not match'),
|
||||
{
|
||||
type: 'info',
|
||||
message: defaultErrorMsg,
|
||||
redirectTo: '/email-signin'
|
||||
}
|
||||
);
|
||||
if (!emailChange || (emailChange && user.newEmail !== email)) {
|
||||
throw wrapHandledError(
|
||||
new Error('user email does not match'),
|
||||
{
|
||||
type: 'info',
|
||||
message: defaultErrorMsg,
|
||||
redirectTo: '/email-signin'
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return authToken.validate$()
|
||||
.map(isValid => {
|
||||
@@ -185,6 +191,13 @@ module.exports = function enableAuthentication(app) {
|
||||
getPasswordlessAuth
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/passwordless-change',
|
||||
ifNoUserRedirectHome,
|
||||
passwordlessGetValidators,
|
||||
getPasswordlessAuth
|
||||
);
|
||||
|
||||
const passwordlessPostValidators = [
|
||||
check('email')
|
||||
.isEmail()
|
||||
|
@@ -7,28 +7,31 @@ import debug from 'debug';
|
||||
import { isEmail } from 'validator';
|
||||
|
||||
import {
|
||||
ifNoUser401,
|
||||
ifNoUserSend
|
||||
ifNoUser401
|
||||
} from '../utils/middleware';
|
||||
|
||||
import { observeQuery } from '../utils/rx';
|
||||
|
||||
import {
|
||||
// legacy
|
||||
frontEndChallengeId,
|
||||
backEndChallengeId,
|
||||
dataVisId,
|
||||
|
||||
// modern
|
||||
respWebDesignId,
|
||||
frontEndLibsId,
|
||||
dataVis2018Id,
|
||||
jsAlgoDataStructId,
|
||||
frontEndChallengeId,
|
||||
dataVisId,
|
||||
apisMicroservicesId,
|
||||
backEndChallengeId,
|
||||
infosecQaId
|
||||
} from '../utils/constantStrings.json';
|
||||
|
||||
import {
|
||||
completeCommitment$
|
||||
} from '../utils/commit';
|
||||
|
||||
import certTypes from '../utils/certTypes.json';
|
||||
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
|
||||
|
||||
const log = debug('fcc:certification');
|
||||
const renderCertifedEmail = loopback.template(path.join(
|
||||
@@ -38,12 +41,9 @@ const renderCertifedEmail = loopback.template(path.join(
|
||||
'emails',
|
||||
'certified.ejs'
|
||||
));
|
||||
const sendMessageToNonUser = ifNoUserSend(
|
||||
'must be logged in to complete.'
|
||||
);
|
||||
|
||||
function isCertified(ids, challengeMap = {}) {
|
||||
return _.every(ids, ({ id }) => challengeMap[id]);
|
||||
return _.every(ids, ({ id }) => _.has(challengeMap, id));
|
||||
}
|
||||
|
||||
function getIdsForCert$(id, Challenge) {
|
||||
@@ -120,12 +120,16 @@ export default function certificate(app) {
|
||||
const { Email, Challenge } = app.models;
|
||||
|
||||
const certTypeIds = {
|
||||
// legacy
|
||||
[certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge),
|
||||
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge),
|
||||
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
|
||||
|
||||
// modern
|
||||
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
|
||||
[certTypes.frontEndLibs]: getIdsForCert$(frontEndLibsId, Challenge),
|
||||
[certTypes.dataVis2018]: getIdsForCert$(dataVis2018Id, Challenge),
|
||||
[certTypes.jsAlgoDataStruct]: getIdsForCert$(jsAlgoDataStructId, Challenge),
|
||||
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
|
||||
[certTypes.apisMicroservices]: getIdsForCert$(
|
||||
apisMicroservicesId,
|
||||
Challenge
|
||||
@@ -133,78 +137,65 @@ export default function certificate(app) {
|
||||
[certTypes.infosecQa]: getIdsForCert$(infosecQaId, Challenge)
|
||||
};
|
||||
|
||||
router.post(
|
||||
'/certificate/verify/front-end',
|
||||
ifNoUser401,
|
||||
verifyCert.bind(null, certTypes.frontEnd)
|
||||
);
|
||||
const superBlocks = Object.keys(superBlockCertTypeMap);
|
||||
|
||||
router.post(
|
||||
'/certificate/verify/back-end',
|
||||
'/certificate/verify',
|
||||
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
|
||||
ifNoSuperBlock404,
|
||||
verifyCert
|
||||
);
|
||||
|
||||
app.use(router);
|
||||
|
||||
function verifyCert(certType, req, res, next) {
|
||||
const { user } = req;
|
||||
const noNameMessage = dedent`
|
||||
We need your name so we can put it on your certificate.
|
||||
Add your name to your account settings and click the save button.
|
||||
Then we can issue your certificate.
|
||||
`;
|
||||
|
||||
const notCertifiedMessage = name => dedent`
|
||||
it looks like you have not completed the neccessary steps.
|
||||
Please complete the required challenges to claim the
|
||||
${name}
|
||||
`;
|
||||
|
||||
const alreadyClaimedMessage = name => dedent`
|
||||
It looks like you already have claimed the ${name}
|
||||
`;
|
||||
|
||||
const successMessage = (username, name) => dedent`
|
||||
@${username}, you have sucessfully claimed
|
||||
the ${name}!
|
||||
Congratulations on behalf of the freeCodeCamp team!
|
||||
`;
|
||||
|
||||
function verifyCert(req, res, next) {
|
||||
const { body: { superBlock }, user } = req;
|
||||
|
||||
let certType = superBlockCertTypeMap[superBlock];
|
||||
log(certType);
|
||||
if (certType === 'isDataVisCert') {
|
||||
certType = 'is2018DataVisCert';
|
||||
log(certType);
|
||||
}
|
||||
return user.getChallengeMap$()
|
||||
.flatMap(() => certTypeIds[certType])
|
||||
.flatMap(challenge => {
|
||||
.flatMap(() => certTypeIds[certType])
|
||||
.flatMap(challenge => {
|
||||
const {
|
||||
id,
|
||||
tests,
|
||||
name,
|
||||
challengeType
|
||||
} = challenge;
|
||||
if (
|
||||
user[certType] ||
|
||||
!isCertified(tests, user.challengeMap)
|
||||
) {
|
||||
return Observable.just(false);
|
||||
if (user[certType]) {
|
||||
return Observable.just(alreadyClaimedMessage(name));
|
||||
}
|
||||
if (!user[certType] && !isCertified(tests, user.challengeMap)) {
|
||||
return Observable.just(notCertifiedMessage(name));
|
||||
}
|
||||
if (!user.name) {
|
||||
return Observable.just(noNameMessage);
|
||||
}
|
||||
const updateData = {
|
||||
$set: {
|
||||
@@ -232,49 +223,32 @@ export default function certificate(app) {
|
||||
sendCertifiedEmail(user, Email.send$),
|
||||
({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage })
|
||||
)
|
||||
.map(
|
||||
.map(
|
||||
({ count, pledgeOrMessage }) => {
|
||||
if (typeof pledgeOrMessage === 'string') {
|
||||
log(pledgeOrMessage);
|
||||
}
|
||||
log(`${count} documents updated`);
|
||||
return true;
|
||||
return successMessage(user.username, name);
|
||||
}
|
||||
);
|
||||
})
|
||||
})
|
||||
.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.
|
||||
`
|
||||
);
|
||||
(message) => {
|
||||
return res.status(200).json({
|
||||
message,
|
||||
success: message.includes('Congratulations')
|
||||
});
|
||||
},
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
function postHonest(req, res, next) {
|
||||
return req.user.update$({ $set: { isHonest: true } }).subscribe(
|
||||
() => res.status(200).send(true),
|
||||
next
|
||||
);
|
||||
function ifNoSuperBlock404(req, res, next) {
|
||||
const { superBlock } = req.body;
|
||||
if (superBlock && superBlocks.includes(superBlock)) {
|
||||
return next();
|
||||
}
|
||||
return res.status(404).end();
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ import { alertTypes } from '../../common/utils/flash.js';
|
||||
|
||||
export default function settingsController(app) {
|
||||
const api = app.loopback.Router();
|
||||
const toggleUserFlag = flag => (req, res, next) => {
|
||||
const toggleUserFlag = (flag, req, res, next) => {
|
||||
const { user } = req;
|
||||
const currentValue = user[ flag ];
|
||||
return user
|
||||
@@ -24,6 +24,15 @@ export default function settingsController(app) {
|
||||
);
|
||||
};
|
||||
|
||||
function refetchChallengeMap(req, res, next) {
|
||||
const { user } = req;
|
||||
return user.requestChallengeMap()
|
||||
.subscribe(
|
||||
challengeMap => res.json({ challengeMap }),
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
const updateMyEmailValidators = [
|
||||
check('email')
|
||||
.isEmail()
|
||||
@@ -39,14 +48,6 @@ export default function settingsController(app) {
|
||||
);
|
||||
}
|
||||
|
||||
api.post(
|
||||
'/update-my-email',
|
||||
ifNoUser401,
|
||||
updateMyEmailValidators,
|
||||
createValidatorErrorHandler(alertTypes.danger),
|
||||
updateMyEmail
|
||||
);
|
||||
|
||||
function updateMyLang(req, res, next) {
|
||||
const { user, body: { lang } = {} } = req;
|
||||
const langName = supportedLanguages[lang];
|
||||
@@ -87,6 +88,94 @@ export default function settingsController(app) {
|
||||
);
|
||||
}
|
||||
|
||||
const updateMyThemeValidators = [
|
||||
check('theme')
|
||||
.isIn(Object.keys(themes))
|
||||
.withMessage('Theme is invalid.')
|
||||
];
|
||||
|
||||
function updateMyTheme(req, res, next) {
|
||||
const { body: { theme } } = req;
|
||||
if (req.user.theme === theme) {
|
||||
return res.sendFlash(alertTypes.info, 'Theme already set');
|
||||
}
|
||||
return req.user.updateTheme(theme)
|
||||
.then(
|
||||
() => res.sendFlash(alertTypes.info, 'Your theme has been updated'),
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
function updateFlags(req, res, next) {
|
||||
const { user, body: { values } } = req;
|
||||
const keys = Object.keys(values);
|
||||
if (
|
||||
keys.length === 1 &&
|
||||
typeof keys[0] === 'boolean'
|
||||
) {
|
||||
return toggleUserFlag(keys[0], req, res, next);
|
||||
}
|
||||
return user.requestUpdateFlags(values)
|
||||
.subscribe(
|
||||
message => res.json({ message }),
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
function updateMyPortfolio(req, res, next) {
|
||||
const {
|
||||
user,
|
||||
body: { portfolio }
|
||||
} = req;
|
||||
// if we only have one key, it should be the id
|
||||
// user cannot send only one key to this route
|
||||
// other than to remove a portfolio item
|
||||
const requestDelete = Object.keys(portfolio).length === 1;
|
||||
return user.updateMyPortfolio(portfolio, requestDelete)
|
||||
.subscribe(
|
||||
message => res.json({ message }),
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
function updateMyProjects(req, res, next) {
|
||||
const {
|
||||
user,
|
||||
body: { projects: project }
|
||||
} = req;
|
||||
return user.updateMyProjects(project)
|
||||
.subscribe(
|
||||
message => res.json({ message }),
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
function updateMyUsername(req, res, next) {
|
||||
const { user, body: { username } } = req;
|
||||
return user.updateMyUsername(username)
|
||||
.subscribe(
|
||||
message => res.json({ message }),
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
api.post(
|
||||
'/refetch-user-challenge-map',
|
||||
ifNoUser401,
|
||||
refetchChallengeMap
|
||||
);
|
||||
api.post(
|
||||
'/update-flags',
|
||||
ifNoUser401,
|
||||
updateFlags
|
||||
);
|
||||
api.post(
|
||||
'/update-my-email',
|
||||
ifNoUser401,
|
||||
updateMyEmailValidators,
|
||||
createValidatorErrorHandler(alertTypes.danger),
|
||||
updateMyEmail
|
||||
);
|
||||
api.post(
|
||||
'/update-my-current-challenge',
|
||||
ifNoUser401,
|
||||
@@ -94,23 +183,21 @@ export default function settingsController(app) {
|
||||
createValidatorErrorHandler(alertTypes.danger),
|
||||
updateMyCurrentChallenge
|
||||
);
|
||||
|
||||
const updateMyThemeValidators = [
|
||||
check('theme')
|
||||
.isIn(Object.keys(themes))
|
||||
.withMessage('Theme is invalid.')
|
||||
];
|
||||
function updateMyTheme(req, res, next) {
|
||||
const { body: { theme } } = req;
|
||||
if (req.user.theme === theme) {
|
||||
return res.sendFlash(alertTypes.info, 'Theme already set');
|
||||
}
|
||||
return req.user.updateTheme(theme)
|
||||
.then(
|
||||
() => res.sendFlash(alertTypes.info, 'Your theme has been updated'),
|
||||
next
|
||||
);
|
||||
}
|
||||
api.post(
|
||||
'/update-my-lang',
|
||||
ifNoUser401,
|
||||
updateMyLang
|
||||
);
|
||||
api.post(
|
||||
'/update-my-portfolio',
|
||||
ifNoUser401,
|
||||
updateMyPortfolio
|
||||
);
|
||||
api.post(
|
||||
'/update-my-projects',
|
||||
ifNoUser401,
|
||||
updateMyProjects
|
||||
);
|
||||
api.post(
|
||||
'/update-my-theme',
|
||||
ifNoUser401,
|
||||
@@ -118,36 +205,10 @@ export default function settingsController(app) {
|
||||
createValidatorErrorHandler(alertTypes.danger),
|
||||
updateMyTheme
|
||||
);
|
||||
|
||||
api.post(
|
||||
'/toggle-available-for-hire',
|
||||
'/update-my-username',
|
||||
ifNoUser401,
|
||||
toggleUserFlag('isAvailableForHire')
|
||||
);
|
||||
api.post(
|
||||
'/toggle-lockdown',
|
||||
ifNoUser401,
|
||||
toggleUserFlag('isLocked')
|
||||
);
|
||||
api.post(
|
||||
'/toggle-announcement-email',
|
||||
ifNoUser401,
|
||||
toggleUserFlag('sendMonthlyEmail')
|
||||
);
|
||||
api.post(
|
||||
'/toggle-notification-email',
|
||||
ifNoUser401,
|
||||
toggleUserFlag('sendNotificationEmail')
|
||||
);
|
||||
api.post(
|
||||
'/toggle-quincy-email',
|
||||
ifNoUser401,
|
||||
toggleUserFlag('sendQuincyEmail')
|
||||
);
|
||||
api.post(
|
||||
'/update-my-lang',
|
||||
ifNoUser401,
|
||||
updateMyLang
|
||||
updateMyUsername
|
||||
);
|
||||
|
||||
app.use(api);
|
||||
|
@@ -2,6 +2,7 @@ import dedent from 'dedent';
|
||||
import moment from 'moment-timezone';
|
||||
import { Observable } from 'rx';
|
||||
import debugFactory from 'debug';
|
||||
// import { curry } from 'lodash';
|
||||
import emoji from 'node-emoji';
|
||||
|
||||
import {
|
||||
@@ -11,10 +12,12 @@ import {
|
||||
frontEndLibsId,
|
||||
jsAlgoDataStructId,
|
||||
dataVisId,
|
||||
dataVis2018Id,
|
||||
apisMicroservicesId,
|
||||
infosecQaId
|
||||
} from '../utils/constantStrings.json';
|
||||
import certTypes from '../utils/certTypes.json';
|
||||
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
|
||||
import {
|
||||
ifNoUser401,
|
||||
ifNoUserRedirectTo,
|
||||
@@ -32,6 +35,7 @@ import { getChallengeInfo, cachedMap } from '../utils/map';
|
||||
|
||||
const debug = debugFactory('fcc:boot:user');
|
||||
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
||||
// const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map');
|
||||
const certIds = {
|
||||
[certTypes.frontEnd]: frontEndChallengeId,
|
||||
[certTypes.backEnd]: backEndChallengeId,
|
||||
@@ -39,6 +43,7 @@ const certIds = {
|
||||
[certTypes.frontEndLibs]: frontEndLibsId,
|
||||
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
|
||||
[certTypes.dataVis]: dataVisId,
|
||||
[certTypes.dataVis2018]: dataVis2018Id,
|
||||
[certTypes.apisMicroservices]: apisMicroservicesId,
|
||||
[certTypes.infosecQa]: infosecQaId
|
||||
};
|
||||
@@ -52,6 +57,7 @@ const certViews = {
|
||||
[certTypes.jsAlgoDataStruct]:
|
||||
'certificate/javascript-algorithms-and-data-structures.jade',
|
||||
[certTypes.dataVis]: 'certificate/data-visualization.jade',
|
||||
[certTypes.dataVis2018]: 'certificate/data-visualization-2018.jade',
|
||||
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
|
||||
[certTypes.infosecQa]:
|
||||
'certificate/information-security-and-quality-assurance.jade'
|
||||
@@ -66,6 +72,7 @@ const certText = {
|
||||
[certTypes.jsAlgoDataStruct]:
|
||||
'JavaScript Algorithms and Data Structures Certified',
|
||||
[certTypes.dataVis]: 'Data Visualization Certified',
|
||||
[certTypes.dataVis2018]: 'Data Visualization Certified',
|
||||
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
|
||||
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
|
||||
};
|
||||
@@ -160,11 +167,6 @@ module.exports = function(app) {
|
||||
);
|
||||
}
|
||||
|
||||
router.get(
|
||||
'/delete-my-account',
|
||||
sendNonUserToMap,
|
||||
showDelete
|
||||
);
|
||||
api.post(
|
||||
'/account/delete',
|
||||
ifNoUser401,
|
||||
@@ -175,17 +177,11 @@ module.exports = function(app) {
|
||||
sendNonUserToMap,
|
||||
getAccount
|
||||
);
|
||||
router.get(
|
||||
'/reset-my-progress',
|
||||
sendNonUserToMap,
|
||||
showResetProgress
|
||||
);
|
||||
api.post(
|
||||
'/account/resetprogress',
|
||||
'/account/reset-progress',
|
||||
ifNoUser401,
|
||||
postResetProgress
|
||||
);
|
||||
|
||||
api.get(
|
||||
'/account/unlink/:social',
|
||||
sendNonUserToMap,
|
||||
@@ -194,48 +190,8 @@ module.exports = function(app) {
|
||||
|
||||
// Ensure these are the last routes!
|
||||
api.get(
|
||||
'/:username/front-end-certification',
|
||||
showCert.bind(null, certTypes.frontEnd)
|
||||
);
|
||||
|
||||
api.get(
|
||||
'/:username/back-end-certification',
|
||||
showCert.bind(null, certTypes.backEnd)
|
||||
);
|
||||
|
||||
api.get(
|
||||
'/:username/full-stack-certification',
|
||||
(req, res) => res.redirect(req.url.replace('full-stack', 'back-end'))
|
||||
);
|
||||
|
||||
api.get(
|
||||
'/:username/responsive-web-design-certification',
|
||||
showCert.bind(null, certTypes.respWebDesign)
|
||||
);
|
||||
|
||||
api.get(
|
||||
'/:username/front-end-libraries-certification',
|
||||
showCert.bind(null, certTypes.frontEndLibs)
|
||||
);
|
||||
|
||||
api.get(
|
||||
'/:username/javascript-algorithms-data-structures-certification',
|
||||
showCert.bind(null, certTypes.jsAlgoDataStruct)
|
||||
);
|
||||
|
||||
api.get(
|
||||
'/:username/data-visualization-certification',
|
||||
showCert.bind(null, certTypes.dataVis)
|
||||
);
|
||||
|
||||
api.get(
|
||||
'/:username/apis-microservices-certification',
|
||||
showCert.bind(null, certTypes.apisMicroservices)
|
||||
);
|
||||
|
||||
api.get(
|
||||
'/:username/information-security-quality-assurance-certification',
|
||||
showCert.bind(null, certTypes.infosecQa)
|
||||
'/c/:username/:cert',
|
||||
showCert
|
||||
);
|
||||
|
||||
router.get('/:username', showUserProfile);
|
||||
@@ -410,14 +366,14 @@ module.exports = function(app) {
|
||||
);
|
||||
}
|
||||
|
||||
function showCert(certType, req, res, next) {
|
||||
const username = req.params.username.toLowerCase();
|
||||
function showCert(req, res, next) {
|
||||
let { username, cert } = req.params;
|
||||
username = username.toLowerCase();
|
||||
const certType = superBlockCertTypeMap[cert];
|
||||
const certId = certIds[certType];
|
||||
return findUserByUsername$(username, {
|
||||
isGithubCool: true,
|
||||
isCheater: true,
|
||||
isLocked: true,
|
||||
isAvailableForHire: true,
|
||||
isFrontEndCert: true,
|
||||
isBackEndCert: true,
|
||||
isFullStackCert: true,
|
||||
@@ -425,6 +381,7 @@ module.exports = function(app) {
|
||||
isFrontEndLibsCert: true,
|
||||
isJsAlgoDataStructCert: true,
|
||||
isDataVisCert: true,
|
||||
is2018DataVisCert: true,
|
||||
isApisMicroservicesCert: true,
|
||||
isInfosecQaCert: true,
|
||||
isHonest: true,
|
||||
@@ -434,6 +391,7 @@ module.exports = function(app) {
|
||||
})
|
||||
.subscribe(
|
||||
user => {
|
||||
const profile = `/${user.username}`;
|
||||
if (!user) {
|
||||
req.flash(
|
||||
'danger',
|
||||
@@ -441,15 +399,16 @@ module.exports = function(app) {
|
||||
);
|
||||
return res.redirect('/');
|
||||
}
|
||||
if (!user.isGithubCool) {
|
||||
|
||||
if (!user.name) {
|
||||
req.flash(
|
||||
'danger',
|
||||
dedent`
|
||||
This user needs to link GitHub with their account
|
||||
This user needs to add their name to their account
|
||||
in order for others to be able to view their certificate.
|
||||
`
|
||||
);
|
||||
return res.redirect('back');
|
||||
return res.redirect(profile);
|
||||
}
|
||||
|
||||
if (user.isCheater) {
|
||||
@@ -465,20 +424,20 @@ module.exports = function(app) {
|
||||
in order for others to be able to view their certificate.
|
||||
`
|
||||
);
|
||||
return res.redirect('back');
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!user.isHonest) {
|
||||
req.flash(
|
||||
'danger',
|
||||
dedent`
|
||||
dedent`
|
||||
${username} has not yet agreed to our Academic Honesty Pledge.
|
||||
`
|
||||
);
|
||||
return res.redirect('back');
|
||||
return res.redirect(profile);
|
||||
}
|
||||
|
||||
if (user[certType]) {
|
||||
|
||||
const { challengeMap = {} } = user;
|
||||
const { completedDate = new Date() } = challengeMap[certId] || {};
|
||||
|
||||
@@ -495,51 +454,49 @@ module.exports = function(app) {
|
||||
'danger',
|
||||
`Looks like user ${username} is not ${certText[certType]}`
|
||||
);
|
||||
return res.redirect('back');
|
||||
return res.redirect(profile);
|
||||
},
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
function showDelete(req, res) {
|
||||
return res.render('account/delete', { title: 'Delete My Account!' });
|
||||
}
|
||||
|
||||
function postDeleteAccount(req, res, next) {
|
||||
User.destroyById(req.user.id, function(err) {
|
||||
if (err) { return next(err); }
|
||||
req.logout();
|
||||
req.flash('info', 'You\'ve successfully deleted your account.');
|
||||
return res.redirect('/');
|
||||
});
|
||||
}
|
||||
|
||||
function showResetProgress(req, res) {
|
||||
return res.render('account/reset-progress', { title: 'Reset My Progress!'
|
||||
req.flash('success', 'You have successfully deleted your account.');
|
||||
return res.status(200).end();
|
||||
});
|
||||
}
|
||||
|
||||
function postResetProgress(req, res, next) {
|
||||
User.findById(req.user.id, function(err, user) {
|
||||
if (err) { return next(err); }
|
||||
return user.updateAttributes({
|
||||
return user.update$({
|
||||
progressTimestamps: [{
|
||||
timestamp: Date.now()
|
||||
}],
|
||||
currentStreak: 0,
|
||||
longestStreak: 0,
|
||||
currentChallengeId: '',
|
||||
isBackEndCert: false,
|
||||
isFullStackCert: false,
|
||||
isDataVisCert: false,
|
||||
isRespWebDesignCert: false,
|
||||
is2018DataVisCert: false,
|
||||
isFrontEndLibsCert: false,
|
||||
isJsAlgoDataStructCert: false,
|
||||
isApisMicroservicesCert: false,
|
||||
isInfosecQaCert: false,
|
||||
is2018FullStackCert: false,
|
||||
isFrontEndCert: false,
|
||||
challengeMap: {},
|
||||
challegesCompleted: []
|
||||
}, function(err) {
|
||||
if (err) { return next(err); }
|
||||
req.flash('info', 'You\'ve successfully reset your progress.');
|
||||
return res.redirect('/');
|
||||
});
|
||||
isBackEndCert: false,
|
||||
isDataVisCert: false,
|
||||
isFullStackCert: false,
|
||||
challengeMap: {}
|
||||
})
|
||||
.subscribe(
|
||||
() => {
|
||||
req.flash('success', 'You have successfully reset your progress.');
|
||||
return res.status(200).end();
|
||||
},
|
||||
next
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,44 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
// import debug from 'debug';
|
||||
// use old rxjs
|
||||
import { Observable } from 'rx';
|
||||
import _ from 'lodash';
|
||||
|
||||
const publicUserProps = [
|
||||
'id',
|
||||
'name',
|
||||
'username',
|
||||
'bio',
|
||||
'theme',
|
||||
'picture',
|
||||
'points',
|
||||
'email',
|
||||
'languageTag',
|
||||
|
||||
'isCheater',
|
||||
'isGithubCool',
|
||||
|
||||
'isLocked',
|
||||
'isAvailableForHire',
|
||||
'isFrontEndCert',
|
||||
'isBackEndCert',
|
||||
'isDataVisCert',
|
||||
'isFullStackCert',
|
||||
'isRespWebDesignCert',
|
||||
'isFrontEndLibsCert',
|
||||
'isJsAlgoDataStructCert',
|
||||
'isApisMicroservicesCert',
|
||||
'isInfosecQaCert',
|
||||
|
||||
'githubURL',
|
||||
'sendMonthlyEmail',
|
||||
'sendNotificationEmail',
|
||||
'sendQuincyEmail',
|
||||
|
||||
'currentChallengeId',
|
||||
'challengeMap'
|
||||
];
|
||||
|
||||
// const log = debug('fcc:services:user');
|
||||
import {
|
||||
userPropsForSession,
|
||||
normaliseUserFields
|
||||
} from '../utils/publicUserProps';
|
||||
|
||||
export default function userServices() {
|
||||
return {
|
||||
@@ -51,18 +17,23 @@ export default function userServices() {
|
||||
Observable.defer(() => user.getChallengeMap$())
|
||||
.map(challengeMap => ({ ...user.toJSON(), challengeMap }))
|
||||
.map(user => ({
|
||||
entities: {
|
||||
user: {
|
||||
[user.username]: {
|
||||
..._.pick(user, publicUserProps),
|
||||
isTwitter: !!user.twitter,
|
||||
isLinkedIn: !!user.linkedIn
|
||||
entities: {
|
||||
user: {
|
||||
[user.username]: {
|
||||
..._.pick(user, userPropsForSession),
|
||||
isEmailVerified: !!user.emailVerified,
|
||||
isGithub: !!user.githubURL,
|
||||
isLinkedIn: !!user.linkedIn,
|
||||
isTwitter: !!user.twitter,
|
||||
isWebsite: !!user.website,
|
||||
...normaliseUserFields(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
result: user.username
|
||||
}))
|
||||
)
|
||||
},
|
||||
result: user.username
|
||||
})
|
||||
)
|
||||
)
|
||||
.subscribe(
|
||||
user => cb(null, user),
|
||||
cb
|
||||
|
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"frontEnd": "isFrontEndCert",
|
||||
"backEnd": "isBackEndCert",
|
||||
"dataVis": "isDataVisCert",
|
||||
"fullStack": "isFullStackCert",
|
||||
"respWebDesign": "isRespWebDesignCert",
|
||||
"frontEndLibs": "isFrontEndLibsCert",
|
||||
"dataVis2018": "is2018DataVisCert",
|
||||
"jsAlgoDataStruct": "isJsAlgoDataStructCert",
|
||||
"dataVis": "isDataVisCert",
|
||||
"apisMicroservices": "isApisMicroservicesCert",
|
||||
"infosecQa": "isInfosecQaCert"
|
||||
}
|
||||
}
|
@@ -1,11 +1,14 @@
|
||||
{
|
||||
"gitHubUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1521.3 Safari/537.36",
|
||||
|
||||
"frontEndChallengeId": "561add10cb82ac38a17513be",
|
||||
"backEndChallengeId": "660add10cb82ac38a17513be",
|
||||
"dataVisId": "561add10cb82ac39a17513bc",
|
||||
|
||||
"respWebDesignId": "561add10cb82ac38a17513bc",
|
||||
"frontEndLibsId": "561acd10cb82ac38a17513bc",
|
||||
"dataVis2018Id": "5a553ca864b52e1d8bceea14",
|
||||
"jsAlgoDataStructId": "561abd10cb81ac38a17513bc",
|
||||
"dataVisId": "561add10cb82ac39a17513bc",
|
||||
"apisMicroservicesId": "561add10cb82ac38a17523bc",
|
||||
"infosecQaId": "561add10cb82ac38a17213bc"
|
||||
}
|
||||
|
@@ -20,3 +20,7 @@ export function unDasherize(name) {
|
||||
.replace(/[^a-zA-Z\d\s]/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function addPlaceholderImage(name) {
|
||||
return `https://identicon.org?t=${name}&s=256`;
|
||||
}
|
||||
|
85
server/utils/publicUserProps.js
Normal file
85
server/utils/publicUserProps.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { isURL } from 'validator';
|
||||
|
||||
import { addPlaceholderImage } from '../utils';
|
||||
import {
|
||||
prepUniqueDaysByHours,
|
||||
calcCurrentStreak,
|
||||
calcLongestStreak
|
||||
} from '../utils/user-stats';
|
||||
|
||||
export const publicUserProps = [
|
||||
'about',
|
||||
'calendar',
|
||||
'challengeMap',
|
||||
'githubURL',
|
||||
'isApisMicroservicesCert',
|
||||
'isBackEndCert',
|
||||
'isCheater',
|
||||
'isDataVisCert',
|
||||
'isFrontEndCert',
|
||||
'isFullStackCert',
|
||||
'isFrontEndLibsCert',
|
||||
'isGithubCool',
|
||||
'isHonest',
|
||||
'isInfosecQaCert',
|
||||
'isJsAlgoDataStructCert',
|
||||
'isLocked',
|
||||
'isRespWebDesignCert',
|
||||
'linkedin',
|
||||
'location',
|
||||
'name',
|
||||
'points',
|
||||
'portfolio',
|
||||
'projects',
|
||||
'streak',
|
||||
'twitter',
|
||||
'username',
|
||||
'website'
|
||||
];
|
||||
|
||||
export const userPropsForSession = [
|
||||
...publicUserProps,
|
||||
'currentChallengeId',
|
||||
'email',
|
||||
'id',
|
||||
'languageTag',
|
||||
'sendQuincyEmail',
|
||||
'theme'
|
||||
];
|
||||
|
||||
export function normaliseUserFields(user) {
|
||||
const about = user.bio && !user.about ? user.bio : user.about;
|
||||
const picture = user.picture || addPlaceholderImage(user.username);
|
||||
const twitter = user.twitter && isURL(user.twitter) ?
|
||||
user.twitter :
|
||||
user.twitter && `https://www.twitter.com/${user.twitter.replace(/^@/, '')}`;
|
||||
return { about, picture, twitter };
|
||||
}
|
||||
|
||||
export function getProgress(progressTimestamps, timezone = 'EST') {
|
||||
const calendar = progressTimestamps
|
||||
.map((objOrNum) => {
|
||||
return typeof objOrNum === 'number' ?
|
||||
objOrNum :
|
||||
objOrNum.timestamp;
|
||||
})
|
||||
.filter((timestamp) => {
|
||||
return !!timestamp;
|
||||
})
|
||||
.reduce((data, timeStamp) => {
|
||||
data[Math.floor(timeStamp / 1000)] = 1;
|
||||
return data;
|
||||
}, {});
|
||||
const timestamps = progressTimestamps
|
||||
.map(objOrNum => {
|
||||
return typeof objOrNum === 'number' ?
|
||||
objOrNum :
|
||||
objOrNum.timestamp;
|
||||
});
|
||||
const uniqueHours = prepUniqueDaysByHours(timestamps, timezone);
|
||||
const streak = {
|
||||
longest: calcLongestStreak(uniqueHours, timezone),
|
||||
current: calcCurrentStreak(uniqueHours, timezone)
|
||||
};
|
||||
return { calendar, streak };
|
||||
}
|
19
server/utils/superBlockCertTypeMap.js
Normal file
19
server/utils/superBlockCertTypeMap.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import certTypes from './certTypes.json';
|
||||
|
||||
const superBlockCertTypeMap = {
|
||||
// legacy
|
||||
'front-end': certTypes.frontEnd,
|
||||
'back-end': certTypes.backEnd,
|
||||
'data-visualization': certTypes.dataVis,
|
||||
'full-stack': certTypes.fullStack,
|
||||
|
||||
// modern
|
||||
'responsive-web-design': certTypes.respWebDesign,
|
||||
'javascript-algorithms-and-data-structures': certTypes.jsAlgoDataStruct,
|
||||
'front-end-libraries': certTypes.frontEndLibs,
|
||||
'data-visualization-2018': certTypes.dataVis2018,
|
||||
'apis-and-microservices': certTypes.apisMicroservices,
|
||||
'information-security-and-quality-assurance': certTypes.infosecQa
|
||||
};
|
||||
|
||||
export default superBlockCertTypeMap;
|
@@ -1,8 +1,8 @@
|
||||
Thank you for updating your contact details.
|
||||
|
||||
Please verify your email by following the link below:
|
||||
Please verify your new email by following the link below:
|
||||
|
||||
<a href="<%= verifyHref %>"><%= verifyHref %></a>
|
||||
<%= host %>/passwordless-change?email=<%= loginEmail %>&token=<%= loginToken %>&emailChange=<%= emailChange %>
|
||||
|
||||
Happy coding!
|
||||
|
||||
|
Reference in New Issue
Block a user