Files
freeCodeCamp/server/boot/user.js

600 lines
16 KiB
JavaScript
Raw Normal View History

import dedent from 'dedent';
import moment from 'moment-timezone';
2015-10-02 11:47:36 -07:00
import { Observable } from 'rx';
2015-08-20 09:40:03 -07:00
import debugFactory from 'debug';
import emoji from 'node-emoji';
import {
2015-12-09 12:28:19 -08:00
frontEndChallengeId,
backEndChallengeId,
respWebDesignId,
frontEndLibsId,
jsAlgoDataStructId,
dataVisId,
apisMicroservicesId,
infosecQaId
} from '../utils/constantStrings.json';
2016-01-11 15:58:37 -08:00
import certTypes from '../utils/certTypes.json';
import {
ifNoUser401,
ifNoUserRedirectTo,
ifNotVerifiedRedirectToSettings
} from '../utils/middleware';
2015-10-02 11:47:36 -07:00
import { observeQuery } from '../utils/rx';
import {
prepUniqueDaysByHours,
calcCurrentStreak,
calcLongestStreak
} from '../utils/user-stats';
import supportedLanguages from '../../common/utils/supported-languages';
import { encodeFcc } from '../../common/utils/encode-decode.js';
import { getChallengeInfo, cachedMap } from '../utils/map';
2016-01-27 11:34:44 -08:00
const debug = debugFactory('fcc:boot:user');
const sendNonUserToMap = ifNoUserRedirectTo('/map');
2016-01-11 15:58:37 -08:00
const certIds = {
[certTypes.frontEnd]: frontEndChallengeId,
[certTypes.backEnd]: backEndChallengeId,
[certTypes.respWebDesign]: respWebDesignId,
[certTypes.frontEndLibs]: frontEndLibsId,
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
[certTypes.dataVis]: dataVisId,
[certTypes.apisMicroservices]: apisMicroservicesId,
[certTypes.infosecQa]: infosecQaId
2016-01-11 15:58:37 -08:00
};
const certViews = {
[certTypes.frontEnd]: 'certificate/front-end.jade',
[certTypes.backEnd]: 'certificate/back-end.jade',
[certTypes.fullStack]: 'certificate/full-stack.jade',
[certTypes.respWebDesign]: 'certificate/responsive-web-design.jade',
[certTypes.frontEndLibs]: 'certificate/front-end-libraries.jade',
[certTypes.jsAlgoDataStruct]:
'certificate/javascript-algorithms-and-data-structures.jade',
[certTypes.dataVis]: 'certificate/data-visualization.jade',
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
[certTypes.infosecQa]:
'certificate/information-security-and-quality-assurance.jade'
2016-01-11 15:58:37 -08:00
};
const certText = {
[certTypes.frontEnd]: 'Front End certified',
2016-01-11 15:58:37 -08:00
[certTypes.backEnd]: 'Back End Certified',
[certTypes.fullStack]: 'Full Stack Certified',
[certTypes.respWebDesign]: 'Responsive Web Design Certified',
[certTypes.frontEndLibs]: 'Front End Libraries Certified',
[certTypes.jsAlgoDataStruct]:
'JavaScript Algorithms and Data Structures Certified',
[certTypes.dataVis]: 'Data Visualization Certified',
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
2016-01-11 15:58:37 -08:00
};
const dateFormat = 'MMM DD, YYYY';
function isAlgorithm(challenge) {
// test if name starts with hike/waypoint/basejump/zipline
// fix for bug that saved different challenges with incorrect
// challenge types
return !(/^(waypoint|hike|zipline|basejump)/i).test(challenge.name) &&
+challenge.challengeType === 5;
}
function isProject(challenge) {
return +challenge.challengeType === 3 ||
+challenge.challengeType === 4;
}
function getChallengeGroup(challenge) {
if (isProject(challenge)) {
return 'projects';
} else if (isAlgorithm(challenge)) {
return 'algorithms';
}
return 'challenges';
}
// buildDisplayChallenges(
// entities: { challenge: Object, challengeIdToName: Object },
// challengeMap: Object,
// tz: String
// ) => Observable[{
// algorithms: Array,
// projects: Array,
// challenges: Array
// }]
function buildDisplayChallenges(
{ challengeMap, challengeIdToName },
userChallengeMap = {},
timezone
) {
return Observable.from(Object.keys(userChallengeMap))
.map(challengeId => userChallengeMap[challengeId])
.map(userChallenge => {
const challengeId = userChallenge.id;
const challenge = challengeMap[ challengeIdToName[challengeId] ];
let finalChallenge = { ...userChallenge, ...challenge };
if (userChallenge.completedDate) {
finalChallenge.completedDate = moment
.tz(userChallenge.completedDate, timezone)
.format(dateFormat);
}
if (userChallenge.lastUpdated) {
finalChallenge.lastUpdated = moment
.tz(userChallenge.lastUpdated, timezone)
.format(dateFormat);
}
return finalChallenge;
})
.filter(({ challengeType }) => challengeType !== 6)
.groupBy(getChallengeGroup)
.flatMap(group$ => {
return group$.toArray().map(challenges => ({
[getChallengeGroup(challenges[0])]: challenges
}));
})
.reduce((output, group) => ({ ...output, ...group}), {})
.map(groups => ({
algorithms: groups.algorithms || [],
projects: groups.projects ? groups.projects.reverse() : [],
challenges: groups.challenges ? groups.challenges.reverse() : []
}));
}
module.exports = function(app) {
2016-06-17 12:35:10 -07:00
const router = app.loopback.Router();
const api = app.loopback.Router();
const { Email, User } = app.models;
const map$ = cachedMap(app.models);
2015-10-02 11:47:36 -07:00
function findUserByUsername$(username, fields) {
return observeQuery(
User,
'findOne',
{
where: { username },
fields
}
);
}
router.get(
'/delete-my-account',
sendNonUserToMap,
showDelete
);
2016-06-17 12:35:10 -07:00
api.post(
2015-08-20 09:40:03 -07:00
'/account/delete',
ifNoUser401,
postDeleteAccount
);
2016-06-17 12:35:10 -07:00
api.get(
'/account',
sendNonUserToMap,
getAccount
);
router.get(
'/reset-my-progress',
sendNonUserToMap,
showResetProgress
);
api.post(
'/account/resetprogress',
ifNoUser401,
postResetProgress
);
2015-10-02 11:47:36 -07:00
api.get(
'/account/unlink/:social',
sendNonUserToMap,
getUnlinkSocial
);
2015-10-02 11:47:36 -07:00
// Ensure these are the last routes!
2016-06-17 12:35:10 -07:00
api.get(
2015-10-02 11:47:36 -07:00
'/:username/front-end-certification',
2016-01-11 15:58:37 -08:00
showCert.bind(null, certTypes.frontEnd)
2015-10-02 11:47:36 -07:00
);
2016-06-17 12:35:10 -07:00
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(
2016-01-11 15:58:37 -08:00
'/:username/data-visualization-certification',
showCert.bind(null, certTypes.dataVis)
2015-12-09 12:28:19 -08:00
);
2016-06-17 12:35:10 -07:00
api.get(
'/:username/apis-microservices-certification',
showCert.bind(null, certTypes.apisMicroservices)
2016-01-11 15:58:37 -08:00
);
2016-06-17 12:35:10 -07:00
api.get(
'/:username/information-security-quality-assurance-certification',
showCert.bind(null, certTypes.infosecQa)
2015-10-02 11:47:36 -07:00
);
router.get('/:username', showUserProfile);
router.get(
'/:username/report-user/',
sendNonUserToMap,
ifNotVerifiedRedirectToSettings,
getReportUserProfile
);
api.post(
'/:username/report-user/',
ifNoUser401,
postReportUserProfile
);
2016-06-17 12:35:10 -07:00
app.use('/:lang', router);
2016-07-15 14:32:42 -07:00
app.use(api);
2015-07-22 23:27:18 -07:00
function getAccount(req, res) {
const { username } = req.user;
return res.redirect('/' + username);
}
function getUnlinkSocial(req, res, next) {
const { user } = req;
const { username } = user;
let social = req.params.social;
if (!social) {
req.flash('danger', 'No social account found');
return res.redirect('/' + username);
}
social = social.toLowerCase();
const validSocialAccounts = ['twitter', 'linkedin'];
if (validSocialAccounts.indexOf(social) === -1) {
req.flash('danger', 'Invalid social account');
return res.redirect('/' + username);
}
if (!user[social]) {
req.flash('danger', `No ${social} account associated`);
return res.redirect('/' + username);
}
const query = {
where: {
provider: social
}
};
return user.identities(query, function(err, identities) {
if (err) { return next(err); }
// assumed user identity is unique by provider
let identity = identities.shift();
if (!identity) {
req.flash('danger', 'No social account found');
return res.redirect('/' + username);
}
return identity.destroy(function(err) {
if (err) { return next(err); }
const updateData = { [social]: null };
return user.update$(updateData)
.subscribe(() => {
debug(`${social} has been unlinked successfully`);
req.flash('info', `You've successfully unlinked your ${social}.`);
return res.redirect('/' + username);
}, next);
});
});
}
function showUserProfile(req, res, next) {
const username = req.params.username.toLowerCase();
2016-06-17 12:35:10 -07:00
const { user } = req;
// timezone of signed-in account
// to show all date related components
// using signed-in account's timezone
// not of the profile she is viewing
const timezone = user && user.timezone ?
user.timezone :
'EST';
const query = {
where: { username },
include: 'pledge'
};
return User.findOne$(query)
.filter(userPortfolio => {
if (!userPortfolio) {
2016-06-17 12:35:10 -07:00
next();
}
return !!userPortfolio;
})
.flatMap(userPortfolio => {
userPortfolio = userPortfolio.toJSON();
const timestamps = userPortfolio
.progressTimestamps
.map(objOrNum => {
return typeof objOrNum === 'number' ?
objOrNum :
objOrNum.timestamp;
});
const uniqueHours = prepUniqueDaysByHours(timestamps, timezone);
userPortfolio.currentStreak = calcCurrentStreak(uniqueHours, timezone);
userPortfolio.longestStreak = calcLongestStreak(uniqueHours, timezone);
const calender = userPortfolio
.progressTimestamps
.map((objOrNum) => {
return typeof objOrNum === 'number' ?
objOrNum :
objOrNum.timestamp;
})
.filter((timestamp) => {
return !!timestamp;
})
.reduce((data, timeStamp) => {
data[(timeStamp / 1000)] = 1;
return data;
}, {});
if (userPortfolio.isCheater && !user) {
req.flash(
'danger',
dedent`
Upon review, this account has been flagged for academic
dishonesty. If youre the owner of this account contact
2017-08-26 00:07:44 +02:00
team@freecodecamp.org for details.
`
);
}
if (userPortfolio.bio) {
userPortfolio.bio = emoji.emojify(userPortfolio.bio);
}
return getChallengeInfo(map$)
.flatMap(challengeInfo => buildDisplayChallenges(
challengeInfo,
userPortfolio.challengeMap,
timezone
))
.map(displayChallenges => ({
...userPortfolio,
...displayChallenges,
title: 'Camper ' + userPortfolio.username + '\'s Code Portfolio',
calender,
github: userPortfolio.githubURL,
moment,
2016-06-17 12:35:10 -07:00
encodeFcc,
supportedLanguages
}));
})
.doOnNext(data => {
return res.render('account/show', data);
})
.subscribe(
() => {},
next
);
}
2016-01-11 15:58:37 -08:00
function showCert(certType, req, res, next) {
2015-10-02 11:47:36 -07:00
const username = req.params.username.toLowerCase();
const certId = certIds[certType];
2016-02-11 13:46:11 -08:00
return findUserByUsername$(username, {
isGithubCool: true,
isCheater: true,
isLocked: true,
2017-06-17 18:09:43 -04:00
isAvailableForHire: true,
2015-10-02 11:47:36 -07:00
isFrontEndCert: true,
2015-12-09 12:28:19 -08:00
isBackEndCert: true,
2016-01-11 15:58:37 -08:00
isFullStackCert: true,
isRespWebDesignCert: true,
isFrontEndLibsCert: true,
isJsAlgoDataStructCert: true,
isDataVisCert: true,
isApisMicroservicesCert: true,
isInfosecQaCert: true,
isHonest: true,
2015-10-02 11:47:36 -07:00
username: true,
name: true,
2016-02-11 13:46:11 -08:00
challengeMap: true
2015-10-02 11:47:36 -07:00
})
.subscribe(
2016-02-11 13:46:11 -08:00
user => {
2015-10-02 11:47:36 -07:00
if (!user) {
req.flash(
'danger',
`We couldn't find a user with the username ${username}`
);
2015-10-02 11:47:36 -07:00
return res.redirect('/');
}
2015-10-05 22:42:42 -07:00
if (!user.isGithubCool) {
req.flash(
'danger',
dedent`
2015-10-05 22:42:42 -07:00
This user needs to link GitHub with their account
2016-01-04 02:01:38 -06:00
in order for others to be able to view their certificate.
2015-10-05 22:42:42 -07:00
`
);
2015-10-05 22:42:42 -07:00
return res.redirect('back');
}
2016-01-24 15:28:15 -08:00
if (user.isCheater) {
return res.redirect(`/${user.username}`);
}
if (user.isLocked) {
req.flash(
'danger',
dedent`
2016-01-10 19:41:50 -06:00
${username} has chosen to make their profile
private. They will need to make their profile public
2016-01-04 02:01:38 -06:00
in order for others to be able to view their certificate.
`
);
2015-10-05 22:42:42 -07:00
return res.redirect('back');
}
if (!user.isHonest) {
req.flash(
'danger',
dedent`
2016-01-04 02:01:38 -06:00
${username} has not yet agreed to our Academic Honesty Pledge.
`
);
2015-10-05 22:42:42 -07:00
return res.redirect('back');
}
2016-01-11 15:58:37 -08:00
if (user[certType]) {
2016-02-11 13:46:11 -08:00
const { challengeMap = {} } = user;
const { completedDate = new Date() } = challengeMap[certId] || {};
2015-10-02 11:47:36 -07:00
return res.render(
2016-01-11 15:58:37 -08:00
certViews[certType],
2015-10-02 11:47:36 -07:00
{
username: user.username,
date: moment(new Date(completedDate)).format('MMMM D, YYYY'),
2015-10-02 11:47:36 -07:00
name: user.name
}
);
}
req.flash(
'danger',
`Looks like user ${username} is not ${certText[certType]}`
);
2016-03-02 20:54:14 -08:00
return res.redirect('back');
2015-10-02 11:47:36 -07:00
},
next
);
}
function showDelete(req, res) {
return res.render('account/delete', { title: 'Delete My Account!' });
}
2015-07-22 23:27:18 -07:00
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.');
2016-03-02 20:54:14 -08:00
return res.redirect('/');
});
}
function showResetProgress(req, res) {
return res.render('account/reset-progress', { title: 'Reset My Progress!'
});
}
function postResetProgress(req, res, next) {
User.findById(req.user.id, function(err, user) {
if (err) { return next(err); }
return user.updateAttributes({
progressTimestamps: [{
timestamp: Date.now()
}],
currentStreak: 0,
longestStreak: 0,
currentChallengeId: '',
isBackEndCert: false,
isFullStackCert: false,
isDataVisCert: false,
isFrontEndCert: false,
challengeMap: {},
challegesCompleted: []
}, function(err) {
if (err) { return next(err); }
req.flash('info', 'You\'ve successfully reset your progress.');
return res.redirect('/');
});
});
}
function getReportUserProfile(req, res) {
const username = req.params.username.toLowerCase();
return res.render('account/report-profile', {
title: 'Report User',
username
});
}
function postReportUserProfile(req, res, next) {
const { user } = req;
const { username } = req.params;
const report = req.sanitize('reportDescription').trimTags();
if (!username || !report || report === '') {
req.flash(
'danger',
'Oops, something is not right please re-check your submission.'
);
return next();
}
return Email.send$({
type: 'email',
2017-08-26 00:07:44 +02:00
to: 'team@freecodecamp.org',
cc: user.email,
2017-08-26 00:07:44 +02:00
from: 'team@freecodecamp.org',
subject: 'Abuse Report : Reporting ' + username + '\'s profile.',
text: dedent(`
Hello Team,\n
This is to report the profile of ${username}.\n
Report Details:\n
${report}\n\n
Reported by:
Username: ${user.username}
Name: ${user.name}
Email: ${user.email}\n
Thanks and regards,
${user.name}
`)
}, err => {
if (err) {
err.redirectTo = '/' + username;
return next(err);
}
req.flash(
'info',
`A report was sent to the team with ${user.email} in copy.`
);
return res.redirect('/');
});
}
2016-12-16 10:35:38 +05:30
};