From d9c3251f946c3e7e80d3ab1ee1a08d36cee0b3ae Mon Sep 17 00:00:00 2001 From: terakilobyte Date: Mon, 1 Jun 2015 18:30:43 -0400 Subject: [PATCH] Begin shaping codebase for loopback migration. --- .eslintrc | 3 +- Procfile | 2 +- README.md | 4 +- controllers/challengeMap.js | 53 -- controllers/resources.js | 641 ------------------ gulpfile.js | 2 +- {controllers => server/boot}/challenge.js | 8 +- server/boot/challengeMap.js | 63 ++ {controllers => server/boot}/fieldGuide.js | 4 +- {controllers => server/boot}/home.js | 12 +- {controllers => server/boot}/jobs.js | 4 +- {controllers => server/boot}/nonprofits.js | 4 +- server/boot/redirects.js | 45 ++ {controllers => server/boot}/story.js | 408 +++++------ {controllers => server/boot}/user.js | 172 ++--- server/boot/utility.js | 435 ++++++++++++ .../resources}/constantStrings.json | 0 server/resources/middleware.js | 34 + server/resources/resources.js | 273 ++++++++ .../resources}/resources.json | 0 app.js => server/server.js | 319 +-------- {views => server/views}/account/account.jade | 0 .../views}/account/email-signin.jade | 0 .../views}/account/email-signup.jade | 0 {views => server/views}/account/forgot.jade | 0 {views => server/views}/account/reset.jade | 0 {views => server/views}/account/show.jade | 0 {views => server/views}/account/signin.jade | 0 .../views}/challengeMap/show.jade | 0 .../views}/coursewares/getstuff.jade | 0 .../views}/coursewares/showBonfire.jade | 0 .../views}/coursewares/showHTML.jade | 0 .../views}/coursewares/showJS.jade | 0 .../views}/coursewares/showVideo.jade | 0 .../coursewares/showZiplineOrBasejump.jade | 0 .../views}/field-guide/all-articles.jade | 0 {views => server/views}/field-guide/show.jade | 0 {views => server/views}/home.jade | 0 {views => server/views}/jobs/directory.jade | 0 {views => server/views}/layout-wide.jade | 0 {views => server/views}/layout.jade | 0 .../views}/nonprofits/directory.jade | 0 {views => server/views}/nonprofits/show.jade | 0 .../views}/partials/challenge-modals.jade | 0 .../views}/partials/css-cdns.jade | 0 .../views}/partials/field-guide.jade | 0 {views => server/views}/partials/flash.jade | 0 {views => server/views}/partials/footer.jade | 0 {views => server/views}/partials/github.jade | 0 {views => server/views}/partials/meta.jade | 0 {views => server/views}/partials/navbar.jade | 0 .../nonprofit-application-progress-bar.jade | 0 .../views}/partials/nonprofits.jade | 0 .../views}/partials/universal-head.jade | 0 .../views}/resources/jobs-form.jade | 0 .../views}/resources/nonprofits-form.jade | 0 .../views}/resources/nonprofits.jade | 0 .../pmi-acp-agile-project-managers-form.jade | 0 .../pmi-acp-agile-project-managers.jade | 0 .../views}/resources/sitemap.jade | 0 {views => server/views}/resources/test.css | 0 {views => server/views}/resources/twitch.jade | 0 .../views}/resources/unsubscribed.jade | 0 {views => server/views}/stories/comments.jade | 0 .../views}/stories/hot-stories.jade | 0 {views => server/views}/stories/index.jade | 10 +- .../views}/stories/new-stories.jade | 0 {views => server/views}/stories/news-nav.jade | 0 .../views}/stories/preliminary-submit.jade | 0 .../views}/stories/search-stories.jade | 0 {views => server/views}/stories/show.jade | 2 +- .../views}/stories/submit-story.jade | 0 setup.js | 6 +- test/app.js | 2 +- 74 files changed, 1186 insertions(+), 1320 deletions(-) delete mode 100644 controllers/challengeMap.js delete mode 100644 controllers/resources.js rename {controllers => server/boot}/challenge.js (98%) create mode 100644 server/boot/challengeMap.js rename {controllers => server/boot}/fieldGuide.js (96%) rename {controllers => server/boot}/home.js (73%) rename {controllers => server/boot}/jobs.js (75%) rename {controllers => server/boot}/nonprofits.js (97%) create mode 100644 server/boot/redirects.js rename {controllers => server/boot}/story.js (61%) rename {controllers => server/boot}/user.js (88%) create mode 100644 server/boot/utility.js rename {controllers => server/resources}/constantStrings.json (100%) create mode 100644 server/resources/middleware.js create mode 100644 server/resources/resources.js rename {controllers => server/resources}/resources.json (100%) rename app.js => server/server.js (52%) rename {views => server/views}/account/account.jade (100%) rename {views => server/views}/account/email-signin.jade (100%) rename {views => server/views}/account/email-signup.jade (100%) rename {views => server/views}/account/forgot.jade (100%) rename {views => server/views}/account/reset.jade (100%) rename {views => server/views}/account/show.jade (100%) rename {views => server/views}/account/signin.jade (100%) rename {views => server/views}/challengeMap/show.jade (100%) rename {views => server/views}/coursewares/getstuff.jade (100%) rename {views => server/views}/coursewares/showBonfire.jade (100%) rename {views => server/views}/coursewares/showHTML.jade (100%) rename {views => server/views}/coursewares/showJS.jade (100%) rename {views => server/views}/coursewares/showVideo.jade (100%) rename {views => server/views}/coursewares/showZiplineOrBasejump.jade (100%) rename {views => server/views}/field-guide/all-articles.jade (100%) rename {views => server/views}/field-guide/show.jade (100%) rename {views => server/views}/home.jade (100%) rename {views => server/views}/jobs/directory.jade (100%) rename {views => server/views}/layout-wide.jade (100%) rename {views => server/views}/layout.jade (100%) rename {views => server/views}/nonprofits/directory.jade (100%) rename {views => server/views}/nonprofits/show.jade (100%) rename {views => server/views}/partials/challenge-modals.jade (100%) rename {views => server/views}/partials/css-cdns.jade (100%) rename {views => server/views}/partials/field-guide.jade (100%) rename {views => server/views}/partials/flash.jade (100%) rename {views => server/views}/partials/footer.jade (100%) rename {views => server/views}/partials/github.jade (100%) rename {views => server/views}/partials/meta.jade (100%) rename {views => server/views}/partials/navbar.jade (100%) rename {views => server/views}/partials/nonprofit-application-progress-bar.jade (100%) rename {views => server/views}/partials/nonprofits.jade (100%) rename {views => server/views}/partials/universal-head.jade (100%) rename {views => server/views}/resources/jobs-form.jade (100%) rename {views => server/views}/resources/nonprofits-form.jade (100%) rename {views => server/views}/resources/nonprofits.jade (100%) rename {views => server/views}/resources/pmi-acp-agile-project-managers-form.jade (100%) rename {views => server/views}/resources/pmi-acp-agile-project-managers.jade (100%) rename {views => server/views}/resources/sitemap.jade (100%) rename {views => server/views}/resources/test.css (100%) rename {views => server/views}/resources/twitch.jade (100%) rename {views => server/views}/resources/unsubscribed.jade (100%) rename {views => server/views}/stories/comments.jade (100%) rename {views => server/views}/stories/hot-stories.jade (100%) rename {views => server/views}/stories/index.jade (83%) rename {views => server/views}/stories/new-stories.jade (100%) rename {views => server/views}/stories/news-nav.jade (100%) rename {views => server/views}/stories/preliminary-submit.jade (100%) rename {views => server/views}/stories/search-stories.jade (100%) rename {views => server/views}/stories/show.jade (99%) rename {views => server/views}/stories/submit-story.jade (100%) diff --git a/.eslintrc b/.eslintrc index 418fe63976..5c50e37af5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,7 +15,8 @@ "window": true, "$": true, "ga": true, - "jQuery": true + "jQuery": true, + "router": true }, "rules": { "no-comma-dangle": 2, diff --git a/Procfile b/Procfile index 6e36002829..7769836542 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: ./node_modules/.bin/forever -m 5 app.js \ No newline at end of file +web: ./node_modules/.bin/forever -m 5 server.js \ No newline at end of file diff --git a/README.md b/README.md index 092c5b76e9..7b4fba7d05 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Project Structure | **views/partials**/footer.jade | Footer partial template. | | **views**/layout.jade | Base template. | | **views**/home.jade | Home page template. | -| app.js | Main application file. | +| server.js | Main application file. | List of Packages @@ -155,7 +155,7 @@ List of Packages | github-api | GitHub API library. | | jade | Template engine for Express. | | less | LESS compiler. Used implicitly by connect-assets. | -| helmet | Restricts Cross site requests. You can modify its settings in app.js | +| helmet | Restricts Cross site requests. You can modify its settings in server.js | | mongoose | MongoDB ODM. | | nodemailer | Node.js library for sending emails. | | passport | Simple and elegant authentication library for node.js | diff --git a/controllers/challengeMap.js b/controllers/challengeMap.js deleted file mode 100644 index 210586f190..0000000000 --- a/controllers/challengeMap.js +++ /dev/null @@ -1,53 +0,0 @@ -var R = require('ramda'), - debug = require('debug')('freecc:cntr:challengeMap'), - User = require('../models/User'), - resources = require('./resources'); - -var challengeTypes = { - 'HTML_CSS_JQ': 0, - 'JAVASCRIPT': 1, - 'VIDEO': 2, - 'ZIPLINE': 3, - 'BASEJUMP': 4, - 'BONFIRE': 5 -}; - -module.exports = { - challengeMap: function challengeMap(req, res, next) { - var completedList = []; - - if (req.user) { - completedList = req.user.completedChallenges; - } - - var noDuplicatedChallenges = R.uniq(completedList); - - var completedChallengeList = noDuplicatedChallenges - .map(function(challenge) { - return challenge._id; - }); - var challengeList = resources.getChallengeMapForDisplay(completedChallengeList); - - function numberWithCommas(x) { - return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); - } - - var date1 = new Date('10/15/2014'); - var date2 = new Date(); - var timeDiff = Math.abs(date2.getTime() - date1.getTime()); - var daysRunning = Math.ceil(timeDiff / (1000 * 3600 * 24)); - - User.count({}, function (err, camperCount) { - if (err) { - return next(err); - } - res.render('challengeMap/show', { - daysRunning: daysRunning, - camperCount: numberWithCommas(camperCount), - title: "A map of all Free Code Camp's Challenges", - challengeList: challengeList, - completedChallengeList: completedChallengeList - }); - }); - } -}; diff --git a/controllers/resources.js b/controllers/resources.js deleted file mode 100644 index e80473d109..0000000000 --- a/controllers/resources.js +++ /dev/null @@ -1,641 +0,0 @@ -var async = require('async'), - path = require('path'), - moment = require('moment'), - Twit = require('twit'), - debug = require('debug')('freecc:cntr:resources'), - cheerio = require('cheerio'), - request = require('request'), - R = require('ramda'), - _ = require('lodash'), - fs = require('fs'), - - - constantStrings = require('./constantStrings.json'), - User = require('../models/User'), - Challenge = require('./../models/Challenge'), - Story = require('./../models/Story'), - FieldGuide = require('./../models/FieldGuide'), - Nonprofit = require('./../models/Nonprofit'), - Comment = require('./../models/Comment'), - resources = require('./resources.json'), - secrets = require('./../config/secrets'), - nonprofits = require('../seed_data/nonprofits.json'), - fieldGuides = require('../seed_data/field-guides.json'), - Slack = require('node-slack'), - slack = new Slack(secrets.slackHook); - -/** - * Cached values - */ -var allFieldGuideIds, allFieldGuideNames, allNonprofitNames, - challengeMap, challengeMapForDisplay, challengeMapWithIds, - challengeMapWithNames, allChallengeIds, allChallenges; - -/** - * GET / - * Resources. - */ - -Array.zip = function(left, right, combinerFunction) { - var counter, - results = []; - - for (counter = 0; counter < Math.min(left.length, right.length); counter++) { - results.push(combinerFunction(left[counter], right[counter])); - } - - return results; -}; - -(function() { - if (!challengeMap) { - var localChallengeMap = {}; - var files = fs.readdirSync( - path.join(__dirname, '/../seed_data/challenges') - ); - var keyCounter = 0; - files = files.map(function (file) { - return require( - path.join(__dirname, '/../seed_data/challenges/' + file) - ); - }); - files = files.sort(function (a, b) { - return a.order - b.order; - }); - files.forEach(function (file) { - localChallengeMap[keyCounter++] = file; - }); - challengeMap = _.cloneDeep(localChallengeMap); - } -})(); - - -module.exports = { - - getChallengeMapForDisplay: function(completedChallengeList) { - if (!challengeMapForDisplay) { - challengeMapForDisplay = {}; - Object.keys(challengeMap).forEach(function(key) { - //TODO get ratio of completed to uncompleted for each section - //challengeMap[key].challenges.forEach(function(challenge){ - // - //} - challengeMapForDisplay[key] = { - name: challengeMap[key].name, - dashedName: challengeMap[key].name.replace(/\s/g, '-'), - challenges: challengeMap[key].challenges, - completedCount: challengeMap[key].challenges //ToDo count number of uncompleted challenges - } - }); - } - return challengeMapForDisplay; - }, - - getChallengeMapWithIds: function() { - if (!challengeMapWithIds) { - challengeMapWithIds = {}; - Object.keys(challengeMap).forEach(function (key) { - var onlyIds = challengeMap[key].challenges.map(function (elem) { - return elem._id; - }); - challengeMapWithIds[key] = onlyIds; - }); - } - return challengeMapWithIds; - }, - - allChallengeIds: function() { - - if (!allChallengeIds) { - allChallengeIds = []; - Object.keys(this.getChallengeMapWithIds()).forEach(function(key) { - allChallengeIds.push(challengeMapWithIds[key]); - }); - allChallengeIds = R.flatten(allChallengeIds); - } - return allChallengeIds; - }, - - allChallenges: function() { - if (!allChallenges) { - allChallenges = []; - Object.keys(this.getChallengeMapWithNames()).forEach(function(key) { - allChallenges.push(challengeMap[key].challenges); - }); - allChallenges = R.flatten(allChallenges); - } - return allChallenges; - }, - - getChallengeMapWithNames: function() { - if (!challengeMapWithNames) { - challengeMapWithNames = {}; - Object.keys(challengeMap). - forEach(function (key) { - var onlyNames = challengeMap[key].challenges.map(function (elem) { - return elem.name; - }); - challengeMapWithNames[key] = onlyNames; - }); - } - return challengeMapWithNames; - }, - - sitemap: function sitemap(req, res, next) { - var appUrl = 'http://www.freecodecamp.com'; - var now = moment(new Date()).format('YYYY-MM-DD'); - - - async.parallel({ - users: function(callback) { - User.aggregate() - .group({_id: 1, usernames: { $addToSet: '$profile.username'}}) - .match({'profile.username': { $ne: ''}}) - .exec(function(err, users) { - if (err) { - debug('User err: ', err); - callback(err); - } else { - callback(null, users[0].usernames); - } - }); - }, - - challenges: function (callback) { - Challenge.aggregate() - .group({_id: 1, names: { $addToSet: '$name'}}) - .exec(function (err, challenges) { - if (err) { - debug('Challenge err: ', err); - callback(err); - } else { - callback(null, challenges[0].names); - } - }); - }, - stories: function (callback) { - Story.aggregate() - .group({_id: 1, links: {$addToSet: '$link'}}) - .exec(function (err, stories) { - if (err) { - debug('Story err: ', err); - callback(err); - } else { - callback(null, stories[0].links); - } - }); - }, - nonprofits: function (callback) { - Nonprofit.aggregate() - .group({_id: 1, names: { $addToSet: '$name'}}) - .exec(function (err, nonprofits) { - if (err) { - debug('User err: ', err); - callback(err); - } else { - callback(null, nonprofits[0].names); - } - }); - }, - fieldGuides: function (callback) { - FieldGuide.aggregate() - .group({_id: 1, names: { $addToSet: '$name'}}) - .exec(function (err, fieldGuides) { - if (err) { - debug('User err: ', err); - callback(err); - } else { - callback(null, fieldGuides[0].names); - } - }); - } - }, function (err, results) { - if (err) { - return next(err); - } else { - setTimeout(function() { - res.header('Content-Type', 'application/xml'); - res.render('resources/sitemap', { - appUrl: appUrl, - now: now, - users: results.users, - challenges: results.challenges, - stories: results.stories, - nonprofits: results.nonprofits, - fieldGuides: results.fieldGuides - }); - }, 0); - } - } - ); - }, - - chat: function chat(req, res) { - if (req.user && req.user.progressTimestamps.length > 5) { - res.redirect('http://freecode.slack.com'); - } else { - res.render('resources/chat', { - title: 'Watch us code live on Twitch.tv' - }); - } - }, - - jobsForm: function jobsForm(req, res) { - res.render('resources/jobs-form', { - title: 'Employer Partnership Form for Job Postings, Recruitment and Corporate Sponsorships' - }); - }, - - catPhotoSubmit: function catPhotoSubmit(req, res) { - res.send( - 'Success! You have submitted your cat photo. Return to your website ' + - 'by typing any letter into your code editor.' - ); - }, - - nonprofits: function nonprofits(req, res) { - res.render('resources/nonprofits', { - title: 'A guide to our Nonprofit Projects' - }); - }, - - nonprofitsForm: function nonprofitsForm(req, res) { - res.render('resources/nonprofits-form', { - title: 'Nonprofit Projects Proposal Form' - }); - }, - - agileProjectManagers: function agileProjectManagers(req, res) { - res.render('resources/pmi-acp-agile-project-managers', { - title: 'Get Agile Project Management Experience for the PMI-ACP' - }); - }, - - agileProjectManagersForm: function agileProjectManagersForm(req, res) { - res.render('resources/pmi-acp-agile-project-managers-form', { - title: 'Agile Project Management Program Application Form' - }); - }, - - twitch: function twitch(req, res) { - res.render('resources/twitch', { - title: "Enter Free Code Camp's Chat Rooms" - }); - }, - - unsubscribe: function unsubscribe(req, res, next) { - User.findOne({ email: req.params.email }, function(err, user) { - if (user) { - if (err) { - return next(err); - } - user.sendMonthlyEmail = false; - user.save(function () { - if (err) { - return next(err); - } - res.redirect('/unsubscribed'); - }); - } else { - res.redirect('/unsubscribed'); - } - }); - }, - - unsubscribed: function unsubscribed(req, res) { - res.render('resources/unsubscribed', { - title: 'You have been unsubscribed' - }); - }, - - githubCalls: function(req, res, next) { - var githubHeaders = { - headers: { - 'User-Agent': constantStrings.gitHubUserAgent - }, - port: 80 - }; - request( - [ - 'https://api.github.com/repos/freecodecamp/', - 'freecodecamp/pulls?client_id=', - secrets.github.clientID, - '&client_secret=', - secrets.github.clientSecret - ].join(''), - githubHeaders, - function(err, status1, pulls) { - if (err) { return next(err); } - pulls = pulls ? - Object.keys(JSON.parse(pulls)).length : - "Can't connect to github"; - - request( - [ - 'https://api.github.com/repos/freecodecamp/', - 'freecodecamp/issues?client_id=', - secrets.github.clientID, - '&client_secret=', - secrets.github.clientSecret - ].join(''), - githubHeaders, - function (err, status2, issues) { - if (err) { return next(err); } - issues = ((pulls === parseInt(pulls, 10)) && issues) ? - Object.keys(JSON.parse(issues)).length - pulls : - "Can't connect to GitHub"; - res.send({ - issues: issues, - pulls: pulls - }); - } - ); - } - ); - }, - - trelloCalls: function(req, res, next) { - request( - 'https://trello.com/1/boards/BA3xVpz9/cards?key=' + - secrets.trello.key, - function(err, status, trello) { - if (err) { return next(err); } - trello = (status && status.statusCode === 200) ? - (JSON.parse(trello)) : - "Can't connect to to Trello"; - - res.end(JSON.stringify(trello)); - }); - }, - - bloggerCalls: function(req, res, next) { - request( - 'https://www.googleapis.com/blogger/v3/blogs/2421288658305323950/' + - 'posts?key=' + - secrets.blogger.key, - function (err, status, blog) { - if (err) { return next(err); } - - blog = (status && status.statusCode === 200) ? - JSON.parse(blog) : - "Can't connect to Blogger"; - res.end(JSON.stringify(blog)); - } - ); - }, - - about: function(req, res, next) { - if (req.user) { - if ( - !req.user.profile.picture || - req.user.profile.picture.indexOf('apple-touch-icon-180x180.png') !== -1 - ) { - req.user.profile.picture = - 'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png'; - // TODO(berks): unhandled callback - req.user.save(); - } - } - var date1 = new Date('10/15/2014'); - var date2 = new Date(); - - var timeDiff = Math.abs(date2.getTime() - date1.getTime()); - var daysRunning = Math.ceil(timeDiff / (1000 * 3600 * 24)); - var announcements = resources.announcements; - function numberWithCommas(x) { - return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); - } - User.count({}, function (err, c3) { - if (err) { - debug('User err: ', err); - return next(err); - } - - res.render('resources/learn-to-code', { - title: 'About Free Code Camp', - daysRunning: daysRunning, - c3: numberWithCommas(c3), - announcements: announcements - }); - }); - }, - - randomPhrase: function() { - return resources.phrases[ - Math.floor(Math.random() * resources.phrases.length) - ]; - }, - - randomVerb: function() { - return resources.verbs[ - Math.floor(Math.random() * resources.verbs.length) - ]; - }, - - randomCompliment: function() { - return resources.compliments[ - Math.floor(Math.random() * resources.compliments.length) - ]; - }, - - allFieldGuideIds: function() { - if (allFieldGuideIds) { - return allFieldGuideIds; - } else { - allFieldGuideIds = fieldGuides.map(function (elem) { - return elem._id; - }); - return allFieldGuideIds; - } - }, - - allFieldGuideNamesAndIds: function() { - if (allFieldGuideNames) { - return allFieldGuideNames; - } else { - allFieldGuideNames = fieldGuides.map(function (elem) { - return { - name: elem.name, - dashedName: elem.dashedName, - id: elem._id }; - }); - return allFieldGuideNames; - } - }, - - allNonprofitNames: function() { - if (allNonprofitNames) { - return allNonprofitNames; - } else { - allNonprofitNames = nonprofits.map(function (elem) { - return { name: elem.name }; - }); - return allNonprofitNames; - } - }, - - whichEnvironment: function() { - return process.env.NODE_ENV; - }, - - getURLTitle: function(url, callback) { - (function () { - var result = {title: '', image: '', url: '', description: ''}; - request(url, function (error, response, body) { - if (!error && response.statusCode === 200) { - var $ = cheerio.load(body); - var metaDescription = $("meta[name='description']"); - var metaImage = $("meta[property='og:image']"); - var urlImage = metaImage.attr('content') ? - metaImage.attr('content') : - ''; - - var metaTitle = $('title'); - var description = metaDescription.attr('content') ? - metaDescription.attr('content') : - ''; - - result.title = metaTitle.text().length < 90 ? - metaTitle.text() : - metaTitle.text().slice(0, 87) + '...'; - - result.image = urlImage; - result.description = description; - callback(null, result); - } else { - callback(new Error('failed')); - } - }); - })(); - }, - - updateUserStoryPictures: function(userId, picture, username, cb) { - - var counter = 0, - foundStories, - foundComments; - - Story.find({'author.userId': userId}, function(err, stories) { - if (err) { - return cb(err); - } - foundStories = stories; - counter++; - saveStoriesAndComments(); - }); - Comment.find({'author.userId': userId}, function(err, comments) { - if (err) { - return cb(err); - } - foundComments = comments; - counter++; - saveStoriesAndComments(); - }); - - function saveStoriesAndComments() { - if (counter !== 2) { - return; - } - var tasks = []; - R.forEach(function(comment) { - comment.author.picture = picture; - comment.author.username = username; - comment.markModified('author'); - tasks.push(function(cb) { - comment.save(cb); - }); - }, foundComments); - - R.forEach(function(story) { - story.author.picture = picture; - story.author.username = username; - story.markModified('author'); - tasks.push(function(cb) { - story.save(cb); - }); - }, foundStories); - async.parallel(tasks, function(err) { - if (err) { - return cb(err); - } - cb(); - }); - } - }, - codepenResources: { - twitter: function(req, res, next) { - // sends out random tweets about javascript - var T = new Twit({ - 'consumer_key': secrets.twitter.consumerKey, - 'consumer_secret': secrets.twitter.consumerSecret, - 'access_token': secrets.twitter.token, - 'access_token_secret': secrets.twitter.tokenSecret - }); - - var screenName; - if (req.params.screenName) { - screenName = req.params.screenName; - } else { - screenName = 'freecodecamp'; - } - - T.get( - 'statuses/user_timeline', - { - 'screen_name': screenName, - count: 10 - }, - function(err, data) { - if (err) { return next(err); } - return res.json(data); - } - ); - }, - twitterFCCStream: function() { - // sends out a tweet stream from FCC's account - }, - twitch: function() { - // exports information from the twitch account - }, - slack: function() { - - } - }, - - getHelp: function(req, res, next) { - var userName = req.user.profile.username; - var code = req.body.payload.code ? '\n```\n' + - req.body.payload.code + '\n```\n' - : ''; - var challenge = req.body.payload.challenge; - - slack.send({ - text: "*@" + userName + "* wants help with " + challenge + ". " + - code + "Hey, *@" + userName + "*, if no one helps you right " + - "away, try typing out your problem in detail to me. Like this: " + - "http://en.wikipedia.org/wiki/Rubber_duck_debugging", - channel: '#help', - username: "Debuggy the Rubber Duck", - icon_url: "https://pbs.twimg.com/profile_images/3609875545/569237541c920fa78d78902069615caf.jpeg" - }); - return res.sendStatus(200); - }, - - getPair: function(req, res, next) { - var userName = req.user.profile.username; - var challenge = req.body.payload.challenge; - slack.send({ - text: "Anyone want to pair with *@" + userName + "* on " + challenge + - "?\nMake sure you install Screen Hero here: " + - "http://freecodecamp.com/field-guide/how-do-i-install-screenhero\n" + - "Then start your pair program session with *@" + userName + - "* by typing \"/hero @" + userName + "\" into Slack.\n And *@"+ userName + - "*, be sure to launch Screen Hero, then keep coding. " + - "Another camper may pair with you soon.", - channel: '#letspair', - username: "Companion Cube", - icon_url: "https://lh3.googleusercontent.com/-f6xDPDV2rPE/AAAAAAAAAAI/AAAAAAAAAAA/mdlESXQu11Q/photo.jpg" - }); - return res.sendStatus(200); - } -}; diff --git a/gulpfile.js b/gulpfile.js index b8568baefc..7a657a2a49 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -9,7 +9,7 @@ var gulp = require('gulp'), eslint = require('gulp-eslint'); var paths = { - server: './app.js', + server: './server.js', serverIgnore: [] }; diff --git a/controllers/challenge.js b/server/boot/challenge.js similarity index 98% rename from controllers/challenge.js rename to server/boot/challenge.js index 12a3c3655e..3b546826e9 100644 --- a/controllers/challenge.js +++ b/server/boot/challenge.js @@ -31,10 +31,10 @@ */ var R = require('ramda'), - Challenge = require('./../models/Challenge'), - User = require('./../models/User'), - resources = require('./resources'), - MDNlinks = require('./../seed_data/bonfireMDNlinks'); + Challenge = require('./../../models/Challenge'), + User = require('./../../models/User'), + resources = require('./../resources/resources'), + MDNlinks = require('./../../seed_data/bonfireMDNlinks'); var challengeMapWithNames = resources.getChallengeMapWithNames(); var challengeMapWithIds = resources.getChallengeMapWithIds(); diff --git a/server/boot/challengeMap.js b/server/boot/challengeMap.js new file mode 100644 index 0000000000..c61c6785a0 --- /dev/null +++ b/server/boot/challengeMap.js @@ -0,0 +1,63 @@ +var R = require('ramda'), + debug = require('debug')('freecc:cntr:challengeMap'), //eslint-disable-line + User = require('../../models/User'), + resources = require('./../resources/resources'), + middleware = require('../resources/middleware'), + express = require('express'), + router = express.Router(); + +router.get('/map', + middleware.userMigration, + challengeMap +); + +router.get('/learn-to-code', function(req, res) { + res.redirect(301, '/map'); +}); + +router.get('/about', function(req, res) { + res.redirect(301, '/map'); +}); + + + +function challengeMap(req, res, next) { + var completedList = []; + + if (req.user) { + completedList = req.user.completedChallenges; + } + + var noDuplicatedChallenges = R.uniq(completedList); + + var completedChallengeList = noDuplicatedChallenges + .map(function(challenge) { + return challenge._id; + }); + var challengeList = resources. + getChallengeMapForDisplay(completedChallengeList); + + function numberWithCommas(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + + var date1 = new Date('10/15/2014'); + var date2 = new Date(); + var timeDiff = Math.abs(date2.getTime() - date1.getTime()); + var daysRunning = Math.ceil(timeDiff / (1000 * 3600 * 24)); + + User.count({}, function (err, camperCount) { + if (err) { + return next(err); + } + res.render('challengeMap/show', { + daysRunning: daysRunning, + camperCount: numberWithCommas(camperCount), + title: "A map of all Free Code Camp's Challenges", + challengeList: challengeList, + completedChallengeList: completedChallengeList + }); + }); +} + +module.exports = router; diff --git a/controllers/fieldGuide.js b/server/boot/fieldGuide.js similarity index 96% rename from controllers/fieldGuide.js rename to server/boot/fieldGuide.js index 5bbd7235b9..c74e993dbd 100644 --- a/controllers/fieldGuide.js +++ b/server/boot/fieldGuide.js @@ -1,6 +1,6 @@ var R = require('ramda'), - FieldGuide = require('./../models/FieldGuide'), - resources = require('./resources'), + FieldGuide = require('./../../models/FieldGuide'), + resources = require('./../resources/resources'), debug = require('debug')('freecc:fieldguides'); exports.returnIndividualFieldGuide = function(req, res, next) { diff --git a/controllers/home.js b/server/boot/home.js similarity index 73% rename from controllers/home.js rename to server/boot/home.js index 86b4a784c3..44efd6c958 100644 --- a/controllers/home.js +++ b/server/boot/home.js @@ -1,7 +1,13 @@ var message = 'Learn to Code JavaScript and get a Coding Job by Helping Nonprofits'; +var express = require('express'); +var router = express.Router(); -exports.index = function(req, res, next) { +router.get('/', index); + + + +function index(req, res, next) { if (req.user && !req.user.profile.picture) { req.user.profile.picture = 'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png'; @@ -13,4 +19,6 @@ exports.index = function(req, res, next) { } else { res.render('home', { title: message }); } -}; +} + +module.exports = router; diff --git a/controllers/jobs.js b/server/boot/jobs.js similarity index 75% rename from controllers/jobs.js rename to server/boot/jobs.js index 5d0cd348cf..1802f4cdf3 100644 --- a/controllers/jobs.js +++ b/server/boot/jobs.js @@ -1,6 +1,6 @@ var moment = require('moment'), - Job = require('./../models/Job'), - resources = require('./resources'); + Job = require('./../../models/Job'), + resources = require('./../resources/resources'); exports.jobsDirectory = function(req, res, next) { Job.find({}, function(err, jobs) { diff --git a/controllers/nonprofits.js b/server/boot/nonprofits.js similarity index 97% rename from controllers/nonprofits.js rename to server/boot/nonprofits.js index d0a3aa0798..176c50cfff 100644 --- a/controllers/nonprofits.js +++ b/server/boot/nonprofits.js @@ -1,6 +1,6 @@ var moment = require('moment'), - Nonprofit = require('./../models/Nonprofit'), - resources = require('./resources'); + Nonprofit = require('./../../models/Nonprofit'), + resources = require('./../resources/resources'); exports.nonprofitsDirectory = function(req, res, next) { Nonprofit.find({estimatedHours: { $gt: 0 } }, function(err, nonprofits) { diff --git a/server/boot/redirects.js b/server/boot/redirects.js new file mode 100644 index 0000000000..674ae9750b --- /dev/null +++ b/server/boot/redirects.js @@ -0,0 +1,45 @@ +var express = require('express'); +var router = express.Router(); + + +router.get('/nonprofit-project-instructions', function(req, res) { + res.redirect(301, '/field-guide/how-do-free-code-camp\'s-nonprofit-projects-work'); +}); + +router.get('/agile', function(req, res) { + res.redirect(301, '/pmi-acp-agile-project-managers'); +}); + +router.get('/live-pair-programming', function(req, res) { + res.redirect(301, '/field-guide/live-stream-pair-programming-on-twitch.tv'); +}); + +router.get('/install-screenhero', function(req, res) { + res.redirect(301, '/field-guide/install-screenhero'); +}); + +router.get('/guide-to-our-nonprofit-projects', function(req, res) { + res.redirect(301, '/field-guide/a-guide-to-our-nonprofit-projects'); +}); + +router.get('/chromebook', function(req, res) { + res.redirect(301, '/field-guide/chromebook'); +}); + +router.get('/deploy-a-website', function(req, res) { + res.redirect(301, '/field-guide/deploy-a-website'); +}); + +router.get('/gmail-shortcuts', function(req, res) { + res.redirect(301, '/field-guide/gmail-shortcuts'); +}); + +router.get('/nodeschool-challenges', function(req, res) { + res.redirect(301, '/field-guide/nodeschool-challenges'); +}); + +router.get('/privacy', function(req, res) { + res.redirect(301, '/field-guide/what-is-the-free-code-camp-privacy-policy?'); +}); + +module.exports = router; diff --git a/controllers/story.js b/server/boot/story.js similarity index 61% rename from controllers/story.js rename to server/boot/story.js index a828f70d05..0902c756ab 100755 --- a/controllers/story.js +++ b/server/boot/story.js @@ -1,16 +1,33 @@ /* eslint-disable no-catch-shadow, no-unused-vars */ var R = require('ramda'), debug = require('debug')('freecc:cntr:story'), - Story = require('./../models/Story'), - Comment = require('./../models/Comment'), - User = require('./../models/User'), + Story = require('./../../models/Story'), + Comment = require('./../../models/Comment'), + User = require('./../../models/User'), moment = require('moment'), - resources = require('./resources'), + resources = require('./../resources/resources'), mongodb = require('mongodb'), MongoClient = mongodb.MongoClient, - secrets = require('../config/secrets'), + secrets = require('../../config/secrets'), nodemailer = require('nodemailer'), - sanitizeHtml = require('sanitize-html'); + sanitizeHtml = require('sanitize-html'), + express = require('express'), + router = express.Router(); + +router.get('/stories/hotStories', hotJSON); +router.get('/stories/recentStories', recentJSON); +router.get('/stories/comments/:id', comments); +router.post('/stories/comment/', commentSubmit); +router.post('/stories/comment/:id/comment', commentOnCommentSubmit); +router.put('/stories/comment/:id/edit', commentEdit); +router.get('/stories/submit', submitNew); +router.get('/stories/submit/new-story', preSubmit); +router.post('/stories/preliminary', newStory); +router.post('/stories/', storySubmission); +router.get('/news/', hot); +router.post('/stories/search', getStories); +router.get('/news/:storyName', returnIndividualStory); +router.post('/stories/upvote/', upvote); function hotRank(timeValue, rank) { /* @@ -27,7 +44,7 @@ function hotRank(timeValue, rank) { } -exports.hotJSON = function(req, res, next) { +function hotJSON(req, res, next) { var story = Story.find({}).sort({'timePosted': -1}).limit(1000); story.exec(function(err, stories) { if (err) { @@ -45,9 +62,9 @@ exports.hotJSON = function(req, res, next) { }).slice(0, sliceVal)); }); -}; +} -exports.recentJSON = function(req, res, next) { +function recentJSON(req, res, next) { var story = Story.find({}).sort({'timePosted': -1}).limit(100); story.exec(function(err, stories) { if (err) { @@ -55,37 +72,37 @@ exports.recentJSON = function(req, res, next) { } return res.json(stories); }); -}; +} -exports.hot = function(req, res) { +function hot(req, res) { return res.render('stories/index', { title: 'Hot stories currently trending on Camper News', page: 'hot' }); -}; +} -exports.submitNew = function(req, res) { +function submitNew(req, res) { return res.render('stories/index', { title: 'Submit a new story to Camper News', page: 'submit' }); -}; +} -exports.search = function(req, res) { +function search(req, res) { return res.render('stories/index', { title: 'Search the archives of Camper News', page: 'search' }); -}; +} -exports.recent = function(req, res) { +function recent(req, res) { return res.render('stories/index', { title: 'Recently submitted stories on Camper News', page: 'recent' }); -}; +} -exports.preSubmit = function(req, res) { +function preSubmit(req, res) { var data = req.query; var cleanData = sanitizeHtml(data.url, { @@ -113,10 +130,10 @@ exports.preSubmit = function(req, res) { storyImage: image, storyMetaDescription: description }); -}; +} -exports.returnIndividualStory = function(req, res, next) { +function returnIndividualStory(req, res, next) { var dashedName = req.params.storyName; var storyName = dashedName.replace(/\-/g, ' ').trim(); @@ -130,7 +147,7 @@ exports.returnIndividualStory = function(req, res, next) { if (story.length < 1) { req.flash('errors', { msg: "404: We couldn't find a story with that name. " + - 'Please double check the name.' + 'Please double check the name.' }); return res.redirect('/news/'); @@ -173,9 +190,9 @@ exports.returnIndividualStory = function(req, res, next) { hasUserVoted: userVoted }); }); -}; +} -exports.getStories = function(req, res, next) { +function getStories(req, res, next) { MongoClient.connect(secrets.db, function(err, database) { if (err) { return next(err); @@ -215,9 +232,9 @@ exports.getStories = function(req, res, next) { return res.sendStatus(404); }); }); -}; +} -exports.upvote = function(req, res, next) { +function upvote(req, res, next) { var data = req.body.data; Story.find({'_id': data.id}, function(err, story) { if (err) { @@ -252,9 +269,9 @@ exports.upvote = function(req, res, next) { }); return res.send(story); }); -}; +} -exports.comments = function(req, res, next) { +function comments(req, res, next) { var data = req.params.id; Comment.find({'_id': data}, function(err, comment) { if (err) { @@ -263,9 +280,9 @@ exports.comments = function(req, res, next) { comment = comment.pop(); return res.send(comment); }); -}; +} -exports.newStory = function(req, res, next) { +function newStory(req, res, next) { if (!req.user) { return next(new Error('Must be logged in')); } @@ -322,9 +339,9 @@ exports.newStory = function(req, res, next) { }); } } -}; +} -exports.storySubmission = function(req, res, next) { +function storySubmission(req, res, next) { var data = req.body.data; if (!req.user) { return next(new Error('Not authorized')); @@ -393,189 +410,190 @@ exports.storySubmission = function(req, res, next) { })); }); }); -}; +} - exports.commentSubmit = function(req, res, next) { - var data = req.body.data; - if (!req.user) { +function commentSubmit(req, res, next) { + var data = req.body.data; + if (!req.user) { + return next(new Error('Not authorized')); + } + var sanitizedBody = sanitizeHtml(data.body, + { + allowedTags: [], + allowedAttributes: [] + }).replace(/"/g, '"'); + if (data.body !== sanitizedBody) { + req.flash('errors', { + msg: 'HTML is not allowed' + }); + return res.send(true); + } + var comment = new Comment({ + associatedPost: data.associatedPost, + originalStoryLink: data.originalStoryLink, + originalStoryAuthorEmail: data.originalStoryAuthorEmail, + body: sanitizedBody, + rank: 0, + upvotes: 0, + author: { + picture: req.user.profile.picture, + userId: req.user._id, + username: req.user.profile.username, + email: req.user.email + }, + comments: [], + topLevel: true, + commentOn: Date.now() + }); + + commentSave(comment, Story, res, next); +} + +function commentOnCommentSubmit(req, res, next) { + var data = req.body.data; + if (!req.user) { + return next(new Error('Not authorized')); + } + + var sanitizedBody = sanitizeHtml(data.body, + { + allowedTags: [], + allowedAttributes: [] + }).replace(/"/g, '"'); + if (data.body !== sanitizedBody) { + req.flash('errors', { + msg: 'HTML is not allowed' + }); + return res.send(true); + } + var comment = new Comment({ + associatedPost: data.associatedPost, + body: sanitizedBody, + rank: 0, + upvotes: 0, + originalStoryLink: data.originalStoryLink, + originalStoryAuthorEmail: data.originalStoryAuthorEmail, + author: { + picture: req.user.profile.picture, + userId: req.user._id, + username: req.user.profile.username, + email: req.user.email + }, + comments: [], + topLevel: false, + commentOn: Date.now() + }); + commentSave(comment, Comment, res, next); +} + +function commentEdit(req, res, next) { + + Comment.find({'_id': req.params.id}, function(err, cmt) { + if (err) { + return next(err); + } + cmt = cmt.pop(); + + if (!req.user && cmt.author.userId !== req.user._id) { return next(new Error('Not authorized')); } - var sanitizedBody = sanitizeHtml(data.body, - { - allowedTags: [], - allowedAttributes: [] - }).replace(/"/g, '"'); - if (data.body !== sanitizedBody) { + + + var sanitizedBody = sanitizeHtml(req.body.body, { + allowedTags: [], + allowedAttributes: [] + }).replace(/"/g, '"'); + if (req.body.body !== sanitizedBody) { req.flash('errors', { msg: 'HTML is not allowed' }); return res.send(true); } - var comment = new Comment({ - associatedPost: data.associatedPost, - originalStoryLink: data.originalStoryLink, - originalStoryAuthorEmail: data.originalStoryAuthorEmail, - body: sanitizedBody, - rank: 0, - upvotes: 0, - author: { - picture: req.user.profile.picture, - userId: req.user._id, - username: req.user.profile.username, - email: req.user.email - }, - comments: [], - topLevel: true, - commentOn: Date.now() - }); - commentSave(comment, Story, res, next); - }; - - exports.commentOnCommentSubmit = function(req, res, next) { - var data = req.body.data; - if (!req.user) { - return next(new Error('Not authorized')); - } - - var sanitizedBody = sanitizeHtml(data.body, - { - allowedTags: [], - allowedAttributes: [] - }).replace(/"/g, '"'); - if (data.body !== sanitizedBody) { - req.flash('errors', { - msg: 'HTML is not allowed' - }); - return res.send(true); - } - var comment = new Comment({ - associatedPost: data.associatedPost, - body: sanitizedBody, - rank: 0, - upvotes: 0, - originalStoryLink: data.originalStoryLink, - originalStoryAuthorEmail: data.originalStoryAuthorEmail, - author: { - picture: req.user.profile.picture, - userId: req.user._id, - username: req.user.profile.username, - email: req.user.email - }, - comments: [], - topLevel: false, - commentOn: Date.now() - }); - commentSave(comment, Comment, res, next); - }; - - exports.commentEdit = function(req, res, next) { - - Comment.find({'_id': req.params.id}, function(err, cmt) { + cmt.body = sanitizedBody; + cmt.commentOn = Date.now(); + cmt.save(function (err) { if (err) { return next(err); } - cmt = cmt.pop(); + res.send(true); + }); - if (!req.user && cmt.author.userId !== req.user._id) { - return next(new Error('Not authorized')); - } + }); +} - var sanitizedBody = sanitizeHtml(req.body.body, { - allowedTags: [], - allowedAttributes: [] - }).replace(/"/g, '"'); - if (req.body.body !== sanitizedBody) { - req.flash('errors', { - msg: 'HTML is not allowed' - }); - return res.send(true); - } - - cmt.body = sanitizedBody; - cmt.commentOn = Date.now(); - cmt.save(function (err) { +function commentSave(comment, Context, res, next) { + comment.save(function(err, data) { + if (err) { + return next(err); + } + try { + // Based on the context retrieve the parent + // object of the comment (Story/Comment) + Context.find({'_id': data.associatedPost}, function (err, associatedContext) { if (err) { return next(err); } - res.send(true); - }); - - }); - - }; - - function commentSave(comment, Context, res, next) { - comment.save(function(err, data) { - if (err) { - return next(err); - } - try { - // Based on the context retrieve the parent - // object of the comment (Story/Comment) - Context.find({'_id': data.associatedPost}, function (err, associatedContext) { - if (err) { - return next(err); - } - associatedContext = associatedContext.pop(); - if (associatedContext) { - associatedContext.comments.push(data._id); - associatedContext.save(function (err) { - if (err) { - return next(err); - } - res.send(true); - }); - } - // Find the author of the parent object - User.findOne({'profile.username': associatedContext.author.username}, function(err, recipient) { + associatedContext = associatedContext.pop(); + if (associatedContext) { + associatedContext.comments.push(data._id); + associatedContext.save(function (err) { if (err) { return next(err); } - // If the emails of both authors differ, - // only then proceed with email notification - if ( - typeof data.author !== 'undefined' && - data.author.email && - typeof recipient !== 'undefined' && - recipient.email && - (data.author.email !== recipient.email) - ) { - var transporter = nodemailer.createTransport({ - service: 'Mandrill', - auth: { - user: secrets.mandrill.user, - pass: secrets.mandrill.password - } - }); - - var mailOptions = { - to: recipient.email, - from: 'Team@freecodecamp.com', - subject: data.author.username + - ' replied to your post on Camper News', - text: [ - 'Just a quick heads-up: ', - data.author.username + ' replied to you on Camper News.', - 'You can keep this conversation going.', - 'Just head back to the discussion here: ', - 'http://freecodecamp.com/news/' + data.originalStoryLink, - '- the Free Code Camp Volunteer Team' - ].join('\n') - }; - - transporter.sendMail(mailOptions, function (err) { - if (err) { - return err; - } - }); - } + res.send(true); }); + } + // Find the author of the parent object + User.findOne({'profile.username': associatedContext.author.username}, function(err, recipient) { + if (err) { + return next(err); + } + // If the emails of both authors differ, + // only then proceed with email notification + if ( + typeof data.author !== 'undefined' && + data.author.email && + typeof recipient !== 'undefined' && + recipient.email && + (data.author.email !== recipient.email) + ) { + var transporter = nodemailer.createTransport({ + service: 'Mandrill', + auth: { + user: secrets.mandrill.user, + pass: secrets.mandrill.password + } + }); + + var mailOptions = { + to: recipient.email, + from: 'Team@freecodecamp.com', + subject: data.author.username + + ' replied to your post on Camper News', + text: [ + 'Just a quick heads-up: ', + data.author.username + ' replied to you on Camper News.', + 'You can keep this conversation going.', + 'Just head back to the discussion here: ', + 'http://freecodecamp.com/news/' + data.originalStoryLink, + '- the Free Code Camp Volunteer Team' + ].join('\n') + }; + + transporter.sendMail(mailOptions, function (err) { + if (err) { + return err; + } + }); + } }); - } catch (e) { - // delete comment - return next(err); - } - }); - } + }); + } catch (e) { + return next(err); + } + }); +} + +module.exports = router; diff --git a/controllers/user.js b/server/boot/user.js similarity index 88% rename from controllers/user.js rename to server/boot/user.js index f83ac9e0be..affcf95e51 100644 --- a/controllers/user.js +++ b/server/boot/user.js @@ -3,68 +3,62 @@ var _ = require('lodash'), crypto = require('crypto'), nodemailer = require('nodemailer'), passport = require('passport'), - User = require('../models/User'), - secrets = require('../config/secrets'), + User = require('../../models/User'), + secrets = require('../../config/secrets'), moment = require('moment'), debug = require('debug')('freecc:cntr:userController'), - resources = require('./resources'), + resources = require('./../resources/resources'), R = require('ramda'); - -/** - * - * @param req - * @param res - * @returns null - * Middleware to migrate users from fragmented challenge structure to unified - * challenge structure - */ -exports.userMigration = function(req, res, next) { - if (req.user && req.user.completedChallenges.length === 0) { - req.user.completedChallenges = R.filter(function (elem) { - return elem; // getting rid of undefined - }, R.concat( - req.user.completedCoursewares, - req.user.completedBonfires.map(function (bonfire) { - return ({ - completedDate: bonfire.completedDate, - _id: bonfire._id, - name: bonfire.name, - completedWith: bonfire.completedWith, - solution: bonfire.solution, - githubLink: '', - verified: false, - challengeType: 5 - }); - }) - )); - next(); - } else { - next(); - } -}; +router.get('/login', function(req, res) { + res.redirect(301, '/signin'); +}); +router.get('/logout', function(req, res) { + res.redirect(301, '/signout'); +}); +router.get('/signin', getSignin); +router.post('/signin', postSignin); +router.get('/signout', signout); +router.get('/forgot', getForgot); +router.post('/forgot', postForgot); +router.get('/reset/:token', getReset); +router.post('/reset/:token', postReset); +router.get('/email-signup', getEmailSignup); +router.get('/email-signin', getEmailSignin); +router.post('/email-signup', postEmailSignup); +router.post('/email-signin', postSignin); +router.get('/account/api', getAccountAngular); +router.get('/api/checkUniqueUsername/:username', checkUniqueUsername); +router.get('/api/checkExistingUsername/:username', checkExistingUsername); +router.get('/api/checkUniqueEmail/:email', checkUniqueEmail); +router.post('/account/profile', postUpdateProfile); +router.post('/account/password', postUpdatePassword); +router.post('/account/delete', postDeleteAccount); +router.get('/account/unlink/:provider', getOauthUnlink); +router.get('/account', getAccount); +router.get('/:username', returnUser); // Ensure this is the last route! /** * GET /signin * Siginin page. */ -exports.getSignin = function(req, res) { +function getSignin (req, res) { if (req.user) { return res.redirect('/'); } res.render('account/signin', { title: 'Free Code Camp Login' }); -}; +} /** * POST /signin * Sign in using email and password. */ -exports.postSignin = function(req, res, next) { +function postSignin (req, res, next) { req.assert('email', 'Email is not valid').isEmail(); req.assert('password', 'Password cannot be blank').notEmpty(); @@ -97,53 +91,52 @@ exports.postSignin = function(req, res, next) { return res.redirect(req.session.returnTo || '/'); }); })(req, res, next); -}; +} /** * GET /signout * Log out. */ -exports.signout = function(req, res) { +function signout (req, res) { req.logout(); res.redirect('/'); -}; +} /** * GET /email-signup * Signup page. */ -exports.getEmailSignin = function(req, res) //noinspection Eslint -{ +function getEmailSignin (req, res) { if (req.user) { return res.redirect('/'); } res.render('account/email-signin', { title: 'Sign in to your Free Code Camp Account' }); -}; +} /** * GET /signin * Signup page. */ -exports.getEmailSignup = function(req, res) { +function getEmailSignup (req, res) { if (req.user) { return res.redirect('/'); } res.render('account/email-signup', { title: 'Create Your Free Code Camp Account' }); -}; +} /** * POST /email-signup * Create a new local account. */ -exports.postEmailSignup = function(req, res, next) { +function postEmailSignup (req, res, next) { req.assert('email', 'valid email required').isEmail(); var errors = req.validationErrors(); @@ -237,34 +230,34 @@ exports.postEmailSignup = function(req, res, next) { }); }); }); -}; +} /** * GET /account * Profile page. */ -exports.getAccount = function(req, res) { +function getAccount (req, res) { res.render('account/account', { title: 'Manage your Free Code Camp Account' }); -}; +} /** * Angular API Call */ -exports.getAccountAngular = function(req, res) { +function getAccountAngular (req, res) { res.json({ user: req.user }); -}; +} /** * Unique username check API Call */ -exports.checkUniqueUsername = function(req, res, next) { +function checkUniqueUsername (req, res, next) { User.count( { 'profile.username': req.params.username.toLowerCase() }, function (err, data) { @@ -275,12 +268,12 @@ exports.checkUniqueUsername = function(req, res, next) { return res.send(false); } }); -}; +} /** * Existing username check */ -exports.checkExistingUsername = function(req, res, next) { +function checkExistingUsername (req, res, next) { User.count( { 'profile.username': req.params.username.toLowerCase() }, function (err, data) { @@ -292,13 +285,13 @@ exports.checkExistingUsername = function(req, res, next) { } } ); -}; +} /** * Unique email check API Call */ -exports.checkUniqueEmail = function(req, res, next) { +function checkUniqueEmail (req, res, next) { User.count( { email: decodeURIComponent(req.params.email).toLowerCase() }, function (err, data) { @@ -310,7 +303,7 @@ exports.checkUniqueEmail = function(req, res, next) { } } ); -}; +} /** @@ -318,7 +311,7 @@ exports.checkUniqueEmail = function(req, res, next) { * Public Profile page. */ -exports.returnUser = function(req, res, next) { +function returnUser (req, res, next) { User.find( { 'profile.username': req.params.username.toLowerCase() }, function(err, user) { @@ -445,37 +438,14 @@ exports.returnUser = function(req, res, next) { } } ); -}; - - -/** - * POST /update-progress - * Update profile information. - */ - -exports.updateProgress = function(req, res, next) { - User.findById(req.user.id, function(err, user) { - if (err) { return next(err); } - user.email = req.body.email || ''; - user.profile.name = req.body.name || ''; - user.profile.gender = req.body.gender || ''; - user.profile.location = req.body.location || ''; - user.profile.website = req.body.website || ''; - - user.save(function(err) { - if (err) { return next(err); } - req.flash('success', { msg: 'Profile information updated.' }); - res.redirect('/account'); - }); - }); -}; +} /** * POST /account/profile * Update profile information. */ -exports.postUpdateProfile = function(req, res, next) { +function postUpdateProfile (req, res, next) { User.findById(req.user.id, function(err) { if (err) { return next(err); } @@ -558,14 +528,14 @@ exports.postUpdateProfile = function(req, res, next) { ); }); }); -}; +} /** * POST /account/password * Update current password. */ -exports.postUpdatePassword = function(req, res, next) { +function postUpdatePassword (req, res, next) { req.assert('password', 'Password must be at least 4 characters long').len(4); req.assert('confirmPassword', 'Passwords do not match') .equals(req.body.password); @@ -589,28 +559,28 @@ exports.postUpdatePassword = function(req, res, next) { res.redirect('/account'); }); }); -}; +} /** * POST /account/delete * Delete user account. */ -exports.postDeleteAccount = function(req, res, next) { +function postDeleteAccount (req, res, next) { User.remove({ _id: req.user.id }, function(err) { if (err) { return next(err); } req.logout(); req.flash('info', { msg: 'Your account has been deleted.' }); res.redirect('/'); }); -}; +} /** * GET /account/unlink/:provider * Unlink OAuth provider. */ -exports.getOauthUnlink = function(req, res, next) { +function getOauthUnlink (req, res, next) { var provider = req.params.provider; User.findById(req.user.id, function(err, user) { if (err) { return next(err); } @@ -627,14 +597,14 @@ exports.getOauthUnlink = function(req, res, next) { res.redirect('/account'); }); }); -}; +} /** * GET /reset/:token * Reset Password page. */ -exports.getReset = function(req, res, next) { +function getReset (req, res, next) { if (req.isAuthenticated()) { return res.redirect('/'); } @@ -654,14 +624,14 @@ exports.getReset = function(req, res, next) { token: req.params.token }); }); -}; +} /** * POST /reset/:token * Process the reset password request. */ -exports.postReset = function(req, res, next) { +function postReset (req, res, next) { var errors = req.validationErrors(); if (errors) { @@ -728,28 +698,28 @@ exports.postReset = function(req, res, next) { if (err) { return next(err); } res.redirect('/'); }); -}; +} /** * GET /forgot * Forgot Password page. */ -exports.getForgot = function(req, res) { +function getForgot (req, res) { if (req.isAuthenticated()) { return res.redirect('/'); } res.render('account/forgot', { title: 'Forgot Password' }); -}; +} /** * POST /forgot * Create a random token, then the send user an email with a reset link. */ -exports.postForgot = function(req, res, next) { +function postForgot (req, res, next) { var errors = req.validationErrors(); if (errors) { @@ -826,4 +796,6 @@ exports.postForgot = function(req, res, next) { if (err) { return next(err); } res.redirect('/forgot'); }); -}; +} + +module.exports = router; diff --git a/server/boot/utility.js b/server/boot/utility.js new file mode 100644 index 0000000000..2565d64b16 --- /dev/null +++ b/server/boot/utility.js @@ -0,0 +1,435 @@ +var express = require('express'), + async = require('async'), + moment = require('moment'), + Twit = require('twit'), + debug = require('debug')('freecc:cntr:resources'), + request = require('request'), + constantStrings = require('./constantStrings.json'), + User = require('../../models/User'), + Challenge = require('./../../models/Challenge'), + Story = require('./../../models/Story'), + FieldGuide = require('./../../models/FieldGuide'), + Nonprofit = require('./../../models/Nonprofit'), + secrets = require('./../../config/secrets'), + Slack = require('node-slack'), + slack = new Slack(secrets.slackHook); +router = express.Router(); + +router.get('/api/github', githubCalls); +router.get('/api/blogger', bloggerCalls); +router.get('/api/trello', trelloCalls); +router.get('/api/codepen/twitter/:screenName', twitter); +router.get('/sitemap.xml', sitemap); +router.post('/get-help', getHelp); +router.post('/get-pair', getPair); +router.get('/chat', chat); +router.get('/twitch', twitch); +router.get('/pmi-acp-agile-project-managers', agileProjectManagers); +router.get('/pmi-acp-agile-project-managers-form', agileProjectManagersForm); +router.get('/nonprofits', nonprofits); +router.get('/nonprofits-form', nonprofitsForm); +router.get('/jobs-form', jobsForm); +router.get('/submit-cat-photo', catPhotoSubmit); +router.get('/unsubscribe/:email', unsubscribe); +router.get('/unsubscribed', unsubscribed); +router.get('/cats.json', getCats); + +router.get('/api/slack', slackInvite); + +function slackInvite(req, res, next) { + if (req.user) { + if (req.user.email) { + var invite = { + 'email': req.user.email, + 'token': process.env.SLACK_KEY, + 'set_active': true + }; + + var headers = { + 'User-Agent': 'Node Browser/0.0.1', + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + var options = { + url: 'https://freecode.slack.com/api/users.admin.invite', + method: 'POST', + headers: headers, + form: invite + }; + + request(options, function (error, response) { + if (!error && response.statusCode === 200) { + req.flash('success', { + msg: 'We\'ve successfully requested an invite for you.' + + ' Please check your email and follow the instructions from Slack.' + }); + req.user.sentSlackInvite = true; + req.user.save(function(err) { + if (err) { + return next(err); + } + return res.redirect('back'); + }); + } else { + req.flash('errors', { + msg: 'The invitation email did not go through for some reason.' + + ' Please try again or ' + + 'email us.' + }); + return res.redirect('back'); + } + }); + } else { + req.flash('notice', { + msg: 'Before we can send your Slack invite, we need your email ' + + 'address. Please update your profile information here.' + }); + return res.redirect('/account'); + } + } else { + req.flash('notice', { + msg: 'You need to sign in to Free Code Camp before ' + + 'we can send you a Slack invite.' + }); + return res.redirect('/account'); + } +} + +function twitter(req, res, next) { + // sends out random tweets about javascript + var T = new Twit({ + 'consumer_key': secrets.twitter.consumerKey, + 'consumer_secret': secrets.twitter.consumerSecret, + 'access_token': secrets.twitter.token, + 'access_token_secret': secrets.twitter.tokenSecret + }); + + var screenName; + if (req.params.screenName) { + screenName = req.params.screenName; + } else { + screenName = 'freecodecamp'; + } + + T.get( + 'statuses/user_timeline', + { + 'screen_name': screenName, + count: 10 + }, + function(err, data) { + if (err) { return next(err); } + return res.json(data); + } + ); +} + + +function getHelp(req, res) { + var userName = req.user.profile.username; + var code = req.body.payload.code ? '\n```\n' + + req.body.payload.code + '\n```\n' + : ''; + var challenge = req.body.payload.challenge; + + slack.send({ + text: '*@' + userName + '* wants help with ' + challenge + '. ' + + code + 'Hey, *@' + userName + '*, if no one helps you right ' + + 'away, try typing out your problem in detail to me. Like this: ' + + 'http://en.wikipedia.org/wiki/Rubber_duck_debugging', + channel: '#help', + username: 'Debuggy the Rubber Duck', + 'icon_url': 'https://pbs.twimg.com/profile_images/' + + '3609875545/569237541c920fa78d78902069615caf.jpeg' + }); + return res.sendStatus(200); +} + +function getPair(req, res) { + var userName = req.user.profile.username; + var challenge = req.body.payload.challenge; + slack.send({ + text: 'Anyone want to pair with *@' + userName + '* on ' + challenge + + '?\nMake sure you install Screen Hero here: ' + + 'http://freecodecamp.com/field-guide/how-do-i-install-screenhero\n' + + 'Then start your pair program session with *@' + userName + + '* by typing \"/hero @' + userName + '\" into Slack.\n And *@'+ userName + + '*, be sure to launch Screen Hero, then keep coding. ' + + 'Another camper may pair with you soon.', + channel: '#letspair', + username: 'Companion Cube', + 'icon_url': 'https://lh3.googleusercontent.com/-f6xDPDV2rPE/AAAAAAAAAAI/' + + 'AAAAAAAAAAA/mdlESXQu11Q/photo.jpg' + }); + return res.sendStatus(200); +} + +function sitemap(req, res, next) { + var appUrl = 'http://www.freecodecamp.com'; + var now = moment(new Date()).format('YYYY-MM-DD'); + + + async.parallel({ + users: function(callback) { + User.aggregate() + .group({_id: 1, usernames: { $addToSet: '$profile.username'}}) + .match({'profile.username': { $ne: ''}}) + .exec(function(err, users) { + if (err) { + debug('User err: ', err); + callback(err); + } else { + callback(null, users[0].usernames); + } + }); + }, + + challenges: function (callback) { + Challenge.aggregate() + .group({_id: 1, names: { $addToSet: '$name'}}) + .exec(function (err, challenges) { + if (err) { + debug('Challenge err: ', err); + callback(err); + } else { + callback(null, challenges[0].names); + } + }); + }, + stories: function (callback) { + Story.aggregate() + .group({_id: 1, links: {$addToSet: '$link'}}) + .exec(function (err, stories) { + if (err) { + debug('Story err: ', err); + callback(err); + } else { + callback(null, stories[0].links); + } + }); + }, + nonprofits: function (callback) { + Nonprofit.aggregate() + .group({_id: 1, names: { $addToSet: '$name'}}) + .exec(function (err, nonprofits) { + if (err) { + debug('User err: ', err); + callback(err); + } else { + callback(null, nonprofits[0].names); + } + }); + }, + fieldGuides: function (callback) { + FieldGuide.aggregate() + .group({_id: 1, names: { $addToSet: '$name'}}) + .exec(function (err, fieldGuides) { + if (err) { + debug('User err: ', err); + callback(err); + } else { + callback(null, fieldGuides[0].names); + } + }); + } + }, function (err, results) { + if (err) { + return next(err); + } else { + setTimeout(function() { + res.header('Content-Type', 'application/xml'); + res.render('resources/sitemap', { + appUrl: appUrl, + now: now, + users: results.users, + challenges: results.challenges, + stories: results.stories, + nonprofits: results.nonprofits, + fieldGuides: results.fieldGuides + }); + }, 0); + } + } + ); +} + +function chat(req, res) { + if (req.user && req.user.progressTimestamps.length > 5) { + res.redirect('http://freecode.slack.com'); + } else { + res.render('resources/chat', { + title: 'Watch us code live on Twitch.tv' + }); + } +} + +function jobsForm(req, res) { + res.render('resources/jobs-form', { + title: 'Employer Partnership Form for Job Postings,' + + ' Recruitment and Corporate Sponsorships' + }); +} + +function catPhotoSubmit(req, res) { + res.send( + 'Success! You have submitted your cat photo. Return to your website ' + + 'by typing any letter into your code editor.' + ); +} + +function nonprofits(req, res) { + res.render('resources/nonprofits', { + title: 'A guide to our Nonprofit Projects' + }); +} + +function nonprofitsForm(req, res) { + res.render('resources/nonprofits-form', { + title: 'Nonprofit Projects Proposal Form' + }); +} + +function agileProjectManagers(req, res) { + res.render('resources/pmi-acp-agile-project-managers', { + title: 'Get Agile Project Management Experience for the PMI-ACP' + }); +} + +function agileProjectManagersForm(req, res) { + res.render('resources/pmi-acp-agile-project-managers-form', { + title: 'Agile Project Management Program Application Form' + }); +} + +function twitch(req, res) { + res.render('resources/twitch', { + title: 'Enter Free Code Camp\'s Chat Rooms' + }); +} + +function unsubscribe(req, res, next) { + User.findOne({ email: req.params.email }, function(err, user) { + if (user) { + if (err) { + return next(err); + } + user.sendMonthlyEmail = false; + user.save(function () { + if (err) { + return next(err); + } + res.redirect('/unsubscribed'); + }); + } else { + res.redirect('/unsubscribed'); + } + }); +} + +function unsubscribed(req, res) { + res.render('resources/unsubscribed', { + title: 'You have been unsubscribed' + }); +} + +function githubCalls(req, res, next) { + var githubHeaders = { + headers: { + 'User-Agent': constantStrings.gitHubUserAgent + }, + port: 80 + }; + request( + [ + 'https://api.github.com/repos/freecodecamp/', + 'freecodecamp/pulls?client_id=', + secrets.github.clientID, + '&client_secret=', + secrets.github.clientSecret + ].join(''), + githubHeaders, + function(err, status1, pulls) { + if (err) { return next(err); } + pulls = pulls ? + Object.keys(JSON.parse(pulls)).length : + 'Can\'t connect to github'; + + request( + [ + 'https://api.github.com/repos/freecodecamp/', + 'freecodecamp/issues?client_id=', + secrets.github.clientID, + '&client_secret=', + secrets.github.clientSecret + ].join(''), + githubHeaders, + function (err, status2, issues) { + if (err) { return next(err); } + issues = ((pulls === parseInt(pulls, 10)) && issues) ? + Object.keys(JSON.parse(issues)).length - pulls : + "Can't connect to GitHub"; + res.send({ + issues: issues, + pulls: pulls + }); + } + ); + } + ); +} + +function trelloCalls(req, res, next) { //eslint-disable-line + request( + 'https://trello.com/1/boards/BA3xVpz9/cards?key=' + + secrets.trello.key, + function(err, status, trello) { + if (err) { return next(err); } + trello = (status && status.statusCode === 200) ? + (JSON.parse(trello)) : + 'Can\'t connect to to Trello'; + + res.end(JSON.stringify(trello)); + }); +} + +function bloggerCalls(req, res, next) { //eslint-disable-line + request( + 'https://www.googleapis.com/blogger/v3/blogs/2421288658305323950/' + + 'posts?key=' + + secrets.blogger.key, + function (err, status, blog) { + if (err) { return next(err); } + + blog = (status && status.statusCode === 200) ? + JSON.parse(blog) : + 'Can\'t connect to Blogger'; + res.end(JSON.stringify(blog)); + } + ); +} + +function getCats(req, res) { //eslint-disable-line + res.send( + [ + { + 'name': 'cute', + 'imageLink': 'https://encrypted-tbn3.gstatic.com/images' + + '?q=tbn:ANd9GcRaP1ecF2jerISkdhjr4R9yM9-8ClUy-TA36MnDiFBukd5IvEME0g' + }, + { + 'name': 'grumpy', + 'imageLink': 'http://cdn.grumpycats.com/wp-content/uploads/' + + '2012/09/GC-Gravatar-copy.png' + }, + { + 'name': 'mischievous', + 'imageLink': 'http://www.kittenspet.com/wp-content' + + '/uploads/2012/08/cat_with_funny_face_3-200x200.jpg' + } + ] + ); +} + +module.exports = router; diff --git a/controllers/constantStrings.json b/server/resources/constantStrings.json similarity index 100% rename from controllers/constantStrings.json rename to server/resources/constantStrings.json diff --git a/server/resources/middleware.js b/server/resources/middleware.js new file mode 100644 index 0000000000..0f2766bfe2 --- /dev/null +++ b/server/resources/middleware.js @@ -0,0 +1,34 @@ +var R = require('ramda'); + +/** + * + * @param req + * @param res + * @returns null + * Middleware to migrate users from fragmented challenge structure to unified + * challenge structure + */ +exports.userMigration = function(req, res, next) { + if (!req.user || req.user.completedChallenges.length !== 0) { + return next(); + } + req.user.completedChallenges = R.filter(function (elem) { + return elem; // getting rid of undefined + }, R.concat( + req.user.completedCoursewares, + req.user.completedBonfires.map(function (bonfire) { + return ({ + completedDate: bonfire.completedDate, + _id: bonfire._id, + name: bonfire.name, + completedWith: bonfire.completedWith, + solution: bonfire.solution, + githubLink: '', + verified: false, + challengeType: 5 + }); + }) + ) + ); + return next(); +}; diff --git a/server/resources/resources.js b/server/resources/resources.js new file mode 100644 index 0000000000..8729b560eb --- /dev/null +++ b/server/resources/resources.js @@ -0,0 +1,273 @@ +var async = require('async'), + path = require('path'), + debug = require('debug')('freecc:cntr:resources'), // eslint-disable-line + cheerio = require('cheerio'), + request = require('request'), + R = require('ramda'), + _ = require('lodash'), + fs = require('fs'), + + + Story = require('./../../models/Story'), + Comment = require('./../../models/Comment'), + resources = require('./resources.json'), + nonprofits = require('../../seed_data/nonprofits.json'), + fieldGuides = require('../../seed_data/field-guides.json'); + +/** + * Cached values + */ +var allFieldGuideIds, allFieldGuideNames, allNonprofitNames, + challengeMap, challengeMapForDisplay, challengeMapWithIds, + challengeMapWithNames, allChallengeIds, allChallenges; + +/** + * GET / + * Resources. + */ + +Array.zip = function(left, right, combinerFunction) { + var counter, + results = []; + + for (counter = 0; counter < Math.min(left.length, right.length); counter++) { + results.push(combinerFunction(left[counter], right[counter])); + } + + return results; +}; + +(function() { + if (!challengeMap) { + var localChallengeMap = {}; + var files = fs.readdirSync( + path.join(__dirname, '/../seed_data/challenges') + ); + var keyCounter = 0; + files = files.map(function (file) { + return require( + path.join(__dirname, '/../seed_data/challenges/' + file) + ); + }); + files = files.sort(function (a, b) { + return a.order - b.order; + }); + files.forEach(function (file) { + localChallengeMap[keyCounter++] = file; + }); + challengeMap = _.cloneDeep(localChallengeMap); + } +})(); + + +module.exports = { + getChallengeMapForDisplay: function () { + if (!challengeMapForDisplay) { + challengeMapForDisplay = {}; + Object.keys(challengeMap).forEach(function (key) { + challengeMapForDisplay[key] = { + name: challengeMap[key].name, + dashedName: challengeMap[key].name.replace(/\s/g, '-'), + challenges: challengeMap[key].challenges, + completedCount: challengeMap[key].challenges + }; + }); + } + return challengeMapForDisplay; + }, + + getChallengeMapWithIds: function () { + if (!challengeMapWithIds) { + challengeMapWithIds = {}; + Object.keys(challengeMap).forEach(function (key) { + var onlyIds = challengeMap[key].challenges.map(function (elem) { + return elem._id; + }); + challengeMapWithIds[key] = onlyIds; + }); + } + return challengeMapWithIds; + }, + + allChallengeIds: function () { + + if (!allChallengeIds) { + allChallengeIds = []; + Object.keys(this.getChallengeMapWithIds()).forEach(function (key) { + allChallengeIds.push(challengeMapWithIds[key]); + }); + allChallengeIds = R.flatten(allChallengeIds); + } + return allChallengeIds; + }, + + allChallenges: function () { + if (!allChallenges) { + allChallenges = []; + Object.keys(this.getChallengeMapWithNames()).forEach(function (key) { + allChallenges.push(challengeMap[key].challenges); + }); + allChallenges = R.flatten(allChallenges); + } + return allChallenges; + }, + + getChallengeMapWithNames: function () { + if (!challengeMapWithNames) { + challengeMapWithNames = {}; + Object.keys(challengeMap). + forEach(function (key) { + var onlyNames = challengeMap[key].challenges.map(function (elem) { + return elem.name; + }); + challengeMapWithNames[key] = onlyNames; + }); + } + return challengeMapWithNames; + }, + + + randomPhrase: function () { + return resources.phrases[ + Math.floor(Math.random() * resources.phrases.length) + ]; + }, + + randomVerb: function () { + return resources.verbs[ + Math.floor(Math.random() * resources.verbs.length) + ]; + }, + + randomCompliment: function () { + return resources.compliments[ + Math.floor(Math.random() * resources.compliments.length) + ]; + }, + + allFieldGuideIds: function () { + if (allFieldGuideIds) { + return allFieldGuideIds; + } else { + allFieldGuideIds = fieldGuides.map(function (elem) { + return elem._id; + }); + return allFieldGuideIds; + } + }, + + allFieldGuideNamesAndIds: function () { + if (allFieldGuideNames) { + return allFieldGuideNames; + } else { + allFieldGuideNames = fieldGuides.map(function (elem) { + return { + name: elem.name, + dashedName: elem.dashedName, + id: elem._id + }; + }); + return allFieldGuideNames; + } + }, + + allNonprofitNames: function () { + if (allNonprofitNames) { + return allNonprofitNames; + } else { + allNonprofitNames = nonprofits.map(function (elem) { + return {name: elem.name}; + }); + return allNonprofitNames; + } + }, + + whichEnvironment: function () { + return process.env.NODE_ENV; + }, + + getURLTitle: function (url, callback) { + (function () { + var result = {title: '', image: '', url: '', description: ''}; + request(url, function (error, response, body) { + if (!error && response.statusCode === 200) { + var $ = cheerio.load(body); + var metaDescription = $("meta[name='description']"); + var metaImage = $("meta[property='og:image']"); + var urlImage = metaImage.attr('content') ? + metaImage.attr('content') : + ''; + + var metaTitle = $('title'); + var description = metaDescription.attr('content') ? + metaDescription.attr('content') : + ''; + + result.title = metaTitle.text().length < 90 ? + metaTitle.text() : + metaTitle.text().slice(0, 87) + '...'; + + result.image = urlImage; + result.description = description; + callback(null, result); + } else { + callback(new Error('failed')); + } + }); + })(); + }, + + updateUserStoryPictures: function (userId, picture, username, cb) { + + var counter = 0, + foundStories, + foundComments; + + Story.find({'author.userId': userId}, function (err, stories) { + if (err) { + return cb(err); + } + foundStories = stories; + counter++; + saveStoriesAndComments(); + }); + Comment.find({'author.userId': userId}, function (err, comments) { + if (err) { + return cb(err); + } + foundComments = comments; + counter++; + saveStoriesAndComments(); + }); + + function saveStoriesAndComments() { + if (counter !== 2) { + return; + } + var tasks = []; + R.forEach(function (comment) { + comment.author.picture = picture; + comment.author.username = username; + comment.markModified('author'); + tasks.push(function (cb) { + comment.save(cb); + }); + }, foundComments); + + R.forEach(function (story) { + story.author.picture = picture; + story.author.username = username; + story.markModified('author'); + tasks.push(function (cb) { + story.save(cb); + }); + }, foundStories); + async.parallel(tasks, function (err) { + if (err) { + return cb(err); + } + cb(); + }); + } + } +}; diff --git a/controllers/resources.json b/server/resources/resources.json similarity index 100% rename from controllers/resources.json rename to server/resources/resources.json diff --git a/app.js b/server/server.js similarity index 52% rename from app.js rename to server/server.js index e431ac361f..107d155398 100755 --- a/app.js +++ b/server/server.js @@ -21,8 +21,8 @@ var express = require('express'), methodOverride = require('method-override'), bodyParser = require('body-parser'), helmet = require('helmet'), - frameguard = require('frameguard'), - csp = require('helmet-csp'), + //frameguard = require('frameguard'), + //csp = require('helmet-csp'), MongoStore = require('connect-mongo')(session), flash = require('express-flash'), path = require('path'), @@ -36,25 +36,23 @@ var express = require('express'), /** * Controllers (route handlers). */ - homeController = require('./controllers/home'), - resourcesController = require('./controllers/resources'), - userController = require('./controllers/user'), - nonprofitController = require('./controllers/nonprofits'), - fieldGuideController = require('./controllers/fieldGuide'), - challengeMapController = require('./controllers/challengeMap'), - challengeController = require('./controllers/challenge'), - jobsController = require('./controllers/jobs'), - - /** - * Stories - */ - storyController = require('./controllers/story'), + homeController = require('./boot/home'), + resourcesController = require('./resources/resources'), + userController = require('./boot/user'), + nonprofitController = require('./boot/nonprofits'), + fieldGuideController = require('./boot/fieldGuide'), + challengeMapController = require('./boot/challengeMap'), + challengeController = require('./boot/challenge'), + jobsController = require('./boot/jobs'), + redirects = require('./boot/redirects'), + utility = require('./boot/utility'), + storyController = require('./boot/story'), /** * API keys and Passport configuration. */ - secrets = require('./config/secrets'), - passportConf = require('./config/passport'); + secrets = require('./../config/secrets'), + passportConf = require('./../config/passport'); /** * Create Express server. @@ -216,128 +214,9 @@ app.use(function (req, res, next) { next(); }); -/** - * Main routes. - */ - -app.get('/', homeController.index); - -app.get('/nonprofit-project-instructions', function(req, res) { - res.redirect(301, '/field-guide/how-do-free-code-camp\'s-nonprofit-projects-work'); -}); - -app.post('/get-help', resourcesController.getHelp); - -app.post('/get-pair', resourcesController.getPair); - -app.get('/chat', resourcesController.chat); - -app.get('/twitch', resourcesController.twitch); - -app.get('/cats.json', function(req, res) { - res.send( - [ - { - "name": "cute", - "imageLink": "https://encrypted-tbn3.gstatic.com/images?q=tbn:ANd9GcRaP1ecF2jerISkdhjr4R9yM9-8ClUy-TA36MnDiFBukd5IvEME0g" - }, - { - "name": "grumpy", - "imageLink": "http://cdn.grumpycats.com/wp-content/uploads/2012/09/GC-Gravatar-copy.png" - }, - { - "name": "mischievous", - "imageLink": "http://www.kittenspet.com/wp-content/uploads/2012/08/cat_with_funny_face_3-200x200.jpg" - } - ] - ) -}); - -// Agile Project Manager Onboarding - -app.get('/pmi-acp-agile-project-managers', - resourcesController.agileProjectManagers); - -app.get('/agile', function(req, res) { - res.redirect(301, '/pmi-acp-agile-project-managers'); -}); - -app.get('/pmi-acp-agile-project-managers-form', - resourcesController.agileProjectManagersForm); - -// Nonprofit Onboarding - -app.get('/nonprofits', resourcesController.nonprofits); - -app.get('/nonprofits-form', resourcesController.nonprofitsForm); - -app.get('/map', - userController.userMigration, - challengeMapController.challengeMap -); - -app.get('/live-pair-programming', function(req, res) { - res.redirect(301, '/field-guide/live-stream-pair-programming-on-twitch.tv'); -}); - -app.get('/install-screenhero', function(req, res) { - res.redirect(301, '/field-guide/install-screenhero'); -}); - -app.get('/guide-to-our-nonprofit-projects', function(req, res) { - res.redirect(301, '/field-guide/a-guide-to-our-nonprofit-projects'); -}); - -app.get('/chromebook', function(req, res) { - res.redirect(301, '/field-guide/chromebook'); -}); - -app.get('/deploy-a-website', function(req, res) { - res.redirect(301, '/field-guide/deploy-a-website'); -}); - -app.get('/gmail-shortcuts', function(req, res) { - res.redirect(301, '/field-guide/gmail-shortcuts'); -}); - -app.get('/nodeschool-challenges', function(req, res) { - res.redirect(301, '/field-guide/nodeschool-challenges'); -}); -app.get('/learn-to-code', challengeMapController.challengeMap); -app.get('/about', function(req, res) { - res.redirect(301, '/map'); -}); -app.get('/signin', userController.getSignin); -app.get('/login', function(req, res) { - res.redirect(301, '/signin'); -}); - -app.post('/signin', userController.postSignin); - -app.get('/signout', userController.signout); - -app.get('/logout', function(req, res) { - res.redirect(301, '/signout'); -}); - -app.get('/forgot', userController.getForgot); - -app.post('/forgot', userController.postForgot); - -app.get('/reset/:token', userController.getReset); - -app.post('/reset/:token', userController.postReset); - -app.get('/email-signup', userController.getEmailSignup); - -app.get('/email-signin', userController.getEmailSignin); - -app.post('/email-signup', userController.postEmailSignup); - -app.post('/email-signin', userController.postSignin); /** * Nonprofit Project routes. @@ -355,169 +234,25 @@ app.get( jobsController.jobsDirectory ); -app.get( - '/jobs-form', - resourcesController.jobsForm -); -app.get('/privacy', function(req, res) { - res.redirect(301, '/field-guide/what-is-the-free-code-camp-privacy-policy?'); -}); -app.get('/submit-cat-photo', resourcesController.catPhotoSubmit); -app.get('/api/slack', function(req, res) { - if (req.user) { - if (req.user.email) { - var invite = { - 'email': req.user.email, - 'token': process.env.SLACK_KEY, - 'set_active': true - }; - var headers = { - 'User-Agent': 'Node Browser/0.0.1', - 'Content-Type': 'application/x-www-form-urlencoded' - }; - var options = { - url: 'https://freecode.slack.com/api/users.admin.invite', - method: 'POST', - headers: headers, - form: invite - }; - - request(options, function (error, response, body) { - if (!error && response.statusCode === 200) { - req.flash('success', { - msg: "We've successfully requested an invite for you. Please check your email and follow the instructions from Slack." - }); - req.user.sentSlackInvite = true; - req.user.save(function(err, user) { - if (err) { - next(err); - } - return res.redirect('back'); - }); - } else { - req.flash('errors', { - msg: "The invitation email did not go through for some reason. Please try again or