diff --git a/README.md b/README.md index 15ba559e4a..dc4e2f7383 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ DEBUG=true mongod # Seed your database with the challenges -node seed_data/seed.js +node seed/ # start the application gulp diff --git a/common/config.global.js b/common/config.global.js new file mode 100644 index 0000000000..fa435086c2 --- /dev/null +++ b/common/config.global.js @@ -0,0 +1,7 @@ +// The path where to mount the REST API app +exports.restApiRoot = '/api'; +// +// The URL where the browser client can access the REST API is available +// Replace with a full url (including hostname) if your client is being +// served from a different server than your REST API. +exports.restApiUrl = exports.restApiRoot; diff --git a/common/models/User-Credential.json b/common/models/User-Credential.json new file mode 100644 index 0000000000..15a271faa4 --- /dev/null +++ b/common/models/User-Credential.json @@ -0,0 +1,16 @@ +{ + "name": "userCredential", + "plural": "userCredentials", + "base": "UserCredential", + "properties": {}, + "validations": [], + "relations": { + "user": { + "type": "belongsTo", + "model": "user", + "foreignKey": "userId" + } + }, + "acls": [], + "methods": [] +} diff --git a/common/models/User-Identity.js b/common/models/User-Identity.js new file mode 100644 index 0000000000..9879ac1f29 --- /dev/null +++ b/common/models/User-Identity.js @@ -0,0 +1,27 @@ +//var debug = require('debug')('freecc:models:userIdent'); +// +//module.exports = function(UserIdent) { +// +// UserIdent.observe('before save', function(ctx, next) { +// +// var userIdent = ctx.instance; +// userIdent.user(function(err, user) { +// if (err) { return next(err); } +// debug('got user', user.username); +// +// // check if user has picture +// // set user.picture from twitter +// if (!user.picture) { +// debug('use has no pic'); +// user.picture = userIdent.profile.photos[0].value; +// user.save(function(err) { +// if (err) { return next(err); } +// next(); +// }); +// } else { +// debug('exiting after user ident'); +// next(); +// } +// }); +// }); +//}; diff --git a/common/models/User-Identity.json b/common/models/User-Identity.json new file mode 100644 index 0000000000..d63e5c0af3 --- /dev/null +++ b/common/models/User-Identity.json @@ -0,0 +1,16 @@ +{ + "name": "userIdentity", + "plural": "userIdentities", + "base": "UserIdentity", + "properties": {}, + "validations": [], + "relations": { + "user": { + "type": "belongsTo", + "model": "user", + "foreignKey": "userId" + } + }, + "acls": [], + "methods": [] +} diff --git a/common/models/User.js b/common/models/User.js new file mode 100644 index 0000000000..83273683f5 --- /dev/null +++ b/common/models/User.js @@ -0,0 +1,19 @@ +var debug = require('debug')('freecc:models:user'); + +module.exports = function(User) { + debug('setting up user hooks'); + /* + * NOTE(berks): not sure if this is still needed + User.observe('before save', function setUsername(ctx, next) { + // set username from twitter + if (ctx.instance.username && ctx.instance.username.match(/twitter/g)) { + ctx.instance.username = + ctx.instance.username.match(/twitter/g) ? + ctx.instance.username.split('.').pop().toLowerCase() : + ctx.instance.username; + debug('username set', ctx.instance.username); + } + next(); + }); + */ +}; diff --git a/common/models/nonprofit.json b/common/models/nonprofit.json index fdc3d673ce..e4347dcc6b 100644 --- a/common/models/nonprofit.json +++ b/common/models/nonprofit.json @@ -18,7 +18,7 @@ "projectDescription": "string", "logoUrl": "string", "imageUrl": "string", - "estimatedHours": 0, + "estimatedHours": "number", "interestedCampers": [], "confirmedCampers": [], "currentStatus": "string" diff --git a/common/models/story.json b/common/models/story.json index 2e46a9f606..2ce2dc5ce4 100644 --- a/common/models/story.json +++ b/common/models/story.json @@ -1,5 +1,5 @@ { - "name": "bonfire", + "name": "story", "base": "PersistedModel", "trackChanges": false, "idInjection": true, diff --git a/common/models/user.json b/common/models/user.json index 5bdab4a1b6..97f8f0b36e 100644 --- a/common/models/user.json +++ b/common/models/user.json @@ -1,6 +1,6 @@ { - "name": "bonfire", - "base": "PersistedModel", + "name": "user", + "base": "User", "trackChanges": false, "idInjection": true, "properties": { diff --git a/common/screens/App.jsx b/common/screens/App.jsx new file mode 100644 index 0000000000..a0756abc14 --- /dev/null +++ b/common/screens/App.jsx @@ -0,0 +1,19 @@ +var React = require('react'), + RouteHandler = require('react-router').RouteHandler, + + // ## components + Nav = require('./nav'), + Footer = require('./footer'); + +var App = React.createClass({ + render: function() { + return ( +
+
+ ); + } +}); +module.exports = App; diff --git a/common/screens/Router.jsx b/common/screens/Router.jsx new file mode 100644 index 0000000000..650c4107ad --- /dev/null +++ b/common/screens/Router.jsx @@ -0,0 +1,34 @@ +var React = require('react'), + + // react router + Router = require('react-router'), + Route = Router.Route, + // NotFound = Router.NotFoundRoute, + DefaultRoute = Router.DefaultRoute, + + // # Components + App = require('./App.jsx'), + Bonfires = require('./bonfires'); + +var routes = ( + + + + + + +); + +module.exports = function(Location) { + return Router.create({ + routes: routes, + location: Location + }); +}; diff --git a/common/screens/bonfires/Actions.js b/common/screens/bonfires/Actions.js new file mode 100644 index 0000000000..1930b48890 --- /dev/null +++ b/common/screens/bonfires/Actions.js @@ -0,0 +1,63 @@ +var Action = require('thundercats').Action, + executeBonfire = require('./executeBonfire'), + getModel = require('../../utils/getModel'), + debug = require('debug')('freecc:common:bonfires'); + +var BonfireActions = Action.createActions([ + 'setUserCode', + 'testUserCode', + 'setResults', + 'setDisplay', + 'setBonfire', + 'getBonfire', + 'handleBonfireError', + 'openCompletionModal' +]); + +BonfireActions + .getBonfire + .subscribe(function(params) { + var Bonfire = getModel('bonfire'); + var bonfireName = params.bonfireName ? + params.bonfireName.replace(/\-/g, ' ') : + 'meet bonfire'; + debug('getting bonfire for: ', bonfireName); + var regQuery = { name: { like: bonfireName, options: 'i' } }; + Bonfire.find( + { where: regQuery }, + function(err, bonfire) { + if (err) { + return debug('bonfire get err', err); + } + if (!bonfire || bonfire.length < 1) { + return debug('404 no bonfire found for ', bonfireName); + } + bonfire = bonfire.pop(); + if (bonfire) { + debug( + 'found bonfire %s for route %s', + bonfire.name, + bonfireName + ); + } + BonfireActions.setBonfire(bonfire); + } + ); + }); + + +BonfireActions + .testUserCode + .subscribe(function({ userCode, tests }) { + debug('test bonfire'); + executeBonfire(userCode, tests, function(err, { output, results }) { + if (err) { + debug('error running tests', err); + return BonfireActions.setDisplay(err); + } + BonfireActions.setDisplay(output); + BonfireActions.setResults(results); + }); + }); + +module.exports = BonfireActions; diff --git a/common/screens/bonfires/Bonfires.jsx b/common/screens/bonfires/Bonfires.jsx new file mode 100644 index 0000000000..9da6db9320 --- /dev/null +++ b/common/screens/bonfires/Bonfires.jsx @@ -0,0 +1,99 @@ +var React = require('react'), + + // ## mixins + { ObservableStateMixin } = require('thundercats'), + + // ## components + SidePanel = require('./SidePanel.jsx'), + Results = require('./Results.jsx'), + Display = require('../displayCode'), + Editor = require('../editor'), + { Grid, Row, Col } = require('react-bootstrap'), + + // ## flux + BonfireActions = require('./Actions'), + BonfireStore = require('./Store'); + +var Bonfire = React.createClass({ + + mixins: [ObservableStateMixin], + + contextTypes: { + makePath: React.PropTypes.func.isRequired, + replaceWith: React.PropTypes.func.isRequired + }, + + getObservable: function() { + return BonfireStore; + }, + + componentDidMount: function() { + // get history object + var his = typeof window !== 'undefined' ? window.history : null; + // spinal-case bonfireName + var bonfireName = this.state.name.toLowerCase().replace(/\s/g, '-'); + // create proper URI from react-router + var path = this.context.makePath('bonfires', { bonfireName: bonfireName }); + + // if html5 push state exists, update URI + // else we are using hash location and should just cause a re render + if (his) { + his.replaceState({ path: path }, '', path); + } else { + this.context.replaceWith('bonfires', { bonfireName: bonfireName}); + } + }, + + _onTestBonfire: function() { + BonfireActions.testUserCode({ + userCode: this.state.userCode, + tests: this.state.tests + }); + }, + + render: function() { + var { + name, + userCode, + difficulty, + description, + results, + display + } = this.state; + var brief = description.slice(0, 1).pop(); + + // convert bonfire difficulty from floating point string + // to integer. + var difficultyInt = Math.floor(+difficulty); + + return ( + + + + 1 ? description : [] }/> + + + + + + + + + ); + } +}); + +module.exports = Bonfire; diff --git a/common/screens/bonfires/Results.jsx b/common/screens/bonfires/Results.jsx new file mode 100644 index 0000000000..8e0e11b449 --- /dev/null +++ b/common/screens/bonfires/Results.jsx @@ -0,0 +1,62 @@ +var React = require('react'), + classNames = require('classnames'), + { Grid, Row, Col } = require('react-bootstrap'); + +var Results = React.createClass({ + + propTypes: { + results: React.PropTypes.array + }, + + _renderText: function(text, textClass) { + return ( + + { text } + + ); + }, + + _renderResult: function(results) { + return results.map(function(result, idx) { + var err = result.err; + var iconClass = { + 'ion-close-circled big-error-icon': err, + 'ion-checkmark-circled big-success-icon': !err + }; + var textClass = { + 'test-output wrappable': true, + 'test-vertical-center': !err + }; + return ( +
+ + + + + { this._renderText(result.text, textClass) } + { err ? this._renderText(err, textClass) : null } + +
+
+ ); + }.bind(this)); + }, + + render: function() { + var results = this.props.results; + if (!results || results.length && results.length === 0) { + return null; + } + return ( + + { this._renderResult(this.props.results) } + + ); + } +}); + +module.exports = Results; diff --git a/common/screens/bonfires/SidePanel.jsx b/common/screens/bonfires/SidePanel.jsx new file mode 100644 index 0000000000..078e9fe72b --- /dev/null +++ b/common/screens/bonfires/SidePanel.jsx @@ -0,0 +1,129 @@ +var React = require('react'), + + // ## components + { + Well, + Row, + Col, + Button, + } = require('react-bootstrap'); + +var SidePanel = React.createClass({ + + propTypes: { + name: React.PropTypes.string, + brief: React.PropTypes.string, + description: React.PropTypes.array, + difficulty: React.PropTypes.number, + onTestBonfire: React.PropTypes.func + }, + + getDefaultProps: function() { + return { + name: 'Welcome to Bonfires!', + difficulty: 5, + brief: 'This is a brief description' + }; + }, + + getInitialState: function() { + return { + isMoreInfoOpen: false + }; + }, + + _toggleMoreInfo: function() { + this.setState({ + isMoreInfoOpen: !this.state.isMoreInfoOpen + }); + }, + + _renderFlames: function() { + var difficulty = this.props.difficulty; + + return [1, 2, 3, 4, 5].map(num => { + var className = 'ion-ios-flame'; + if (num > difficulty) { + className += '-outline'; + } + return ( + + ); + }); + }, + + _renderMoreInfo: function(isDescription) { + var description = this.props.description.map((sentance, index) => { + return

{ sentance }

; + }); + + if (isDescription && this.state.isMoreInfoOpen) { + return ( + + + { description } + + + ); + } + return null; + }, + + _renderMoreInfoButton: function(isDescription) { + if (isDescription) { + return ( + + ); + } + return null; + }, + + render: function() { + var isDescription = this.props.description && + this.props.description.length > 1; + + return ( +
+

{ this.props.name }

+

+
+ Difficulty:  + { this._renderFlames() } +
+

+ + + +
+

{ this.props.brief }

+
+ { this._renderMoreInfo(isDescription) } + { this._renderMoreInfoButton(isDescription) } +
+
+ +
+
+ +
+
+ ); + } +}); + +module.exports = SidePanel; diff --git a/common/screens/bonfires/Store.js b/common/screens/bonfires/Store.js new file mode 100644 index 0000000000..4a83c223d6 --- /dev/null +++ b/common/screens/bonfires/Store.js @@ -0,0 +1,67 @@ +var BonfiresActions = require('./Actions'); +var { Store, setStateUtil } = require('thundercats'); + +var BonfiresStore = Store.create({ + + getInitialValue: function() { + return { + userCode: 'console.log(\'FreeCodeCamp!\')', + difficulty: 0, + description: [ + 'default state' + ], + tests: [], + results: null + }; + }, + + getOperations: function() { + var { + setBonfire, + setUserCode, + setResults, + setDisplay + } = BonfiresActions; + + return [ + setBonfire + .map(function(bonfire) { + var { + name, + description, + difficulty, + tests + } = bonfire; + var userCode = bonfire.challengeSeed; + return { + name, + userCode, + tests, + description, + difficulty + }; + }) + .map(setStateUtil), + + setUserCode + .map(function(userCode) { + return { userCode }; + }) + .map(setStateUtil), + + setDisplay + .map(function(display) { + return { display }; + }) + .map(setStateUtil), + + setResults + .map(function(results) { + return { results }; + }) + .map(setStateUtil) + ]; + } +}); + +module.exports = BonfiresStore; diff --git a/common/screens/bonfires/executeBonfire.js b/common/screens/bonfires/executeBonfire.js new file mode 100644 index 0000000000..0d1795894a --- /dev/null +++ b/common/screens/bonfires/executeBonfire.js @@ -0,0 +1,27 @@ +var debug = require('debug')('freecc:executebonfire'); +var { + addTests, + runTests, + testCode +} = require('../../utils'); + +module.exports = executeBonfire; + +function executeBonfire(userCode, tests, cb) { + + // TODO: move this into componentDidMount + // ga('send', 'event', 'Bonfire', 'ran-code', bonfireName); + var testSalt = Math.random(); + var { preppedCode, userTests } = addTests(userCode, tests, testSalt); + + debug('sending code to web worker for testing'); + testCode(preppedCode, function(err, data) { + if (err) { return cb(err); } + var results = runTests(userTests, data, testSalt); + debug('testing complete', results); + cb(null, { + output: data.output, + results + }); + }); +} diff --git a/common/screens/bonfires/index.js b/common/screens/bonfires/index.js new file mode 100644 index 0000000000..5557fd9f16 --- /dev/null +++ b/common/screens/bonfires/index.js @@ -0,0 +1 @@ +module.exports = require('./Bonfires.jsx'); diff --git a/common/screens/context/Actions.js b/common/screens/context/Actions.js new file mode 100644 index 0000000000..83e06c38e1 --- /dev/null +++ b/common/screens/context/Actions.js @@ -0,0 +1,34 @@ +var debug = require('debug')('freecc:context'), + BonfireActions = require('../bonfires/Actions'), + BonfireStore = require('../bonfires/Store'); + +var { + Action, + waitFor +} = require('thundercats'); + +var actions = Action.createActions([ + 'setContext', + 'renderToUser' +]); + +actions + .setContext + .filter(function(ctx) { + return ctx.state.path.indexOf('/bonfire') !== -1; + }) + .subscribe(function(ctx) { + debug('set ctx'); + BonfireActions.getBonfire(ctx.state.params); + waitFor(BonfireStore) + .firstOrDefault() + .catch(function(err) { + // handle timeout error + debug('err', err); + }) + .subscribe(function() { + actions.renderToUser(ctx); + }); + }); + +module.exports = actions; diff --git a/common/screens/context/Store.js b/common/screens/context/Store.js new file mode 100644 index 0000000000..f94308ace1 --- /dev/null +++ b/common/screens/context/Store.js @@ -0,0 +1,18 @@ +var Store = require('thundercats').Store, + ContextActions = require('./Actions'); + +var ContextStore = Store.create({ + getInitialValue: function() { + return {}; + }, + + getOperations: function() { + return ContextActions + .renderToUser + .map(function(ctx) { + return { value: ctx }; + }); + } +}); + +module.exports = ContextStore; diff --git a/common/screens/displayCode/Display.jsx b/common/screens/displayCode/Display.jsx new file mode 100644 index 0000000000..9eb60db8da --- /dev/null +++ b/common/screens/displayCode/Display.jsx @@ -0,0 +1,51 @@ +var React = require('react'), + Tailspin = require('tailspin'); + +var Editor = React.createClass({ + + propTypes: { + value: React.PropTypes.string + }, + + getDefaultProps: function() { + return { + value: [ + '/**', + '* Your output will go here.', + '* Console.log() -type statements', + '* will appear in your browser\'s', + '* DevTools JavaScript console.', + '**/' + ].join('\n') + }; + }, + + render: function() { + var value = this.props.value; + var options = { + lineNumbers: false, + lineWrapping: true, + mode: 'text', + readOnly: 'noCursor', + textAreaClassName: 'hide-textarea', + theme: 'monokai', + value: value + }; + + var config = { + setSize: ['100%', '100%'] + }; + + return ( +
+
+ +
+
+ ); + } +}); + +module.exports = Editor; diff --git a/common/screens/displayCode/index.js b/common/screens/displayCode/index.js new file mode 100644 index 0000000000..d3e3560174 --- /dev/null +++ b/common/screens/displayCode/index.js @@ -0,0 +1 @@ +module.exports = require('./Display.jsx'); diff --git a/common/screens/editor/Editor.jsx b/common/screens/editor/Editor.jsx new file mode 100644 index 0000000000..8e72f5d4a0 --- /dev/null +++ b/common/screens/editor/Editor.jsx @@ -0,0 +1,91 @@ +var React = require('react'), + debug = require('debug')('freecc:comp:editor'), + jshint = require('jshint').JSHINT, + Tailspin = require('tailspin'); + +var Editor = React.createClass({ + + propTypes: { + onValueChange: React.PropTypes.func, + value: React.PropTypes.string + }, + + getDefaultProps: function() { + return { + value: 'console.log(\'freeCodeCamp is awesome\')' + }; + }, + + getInitialState: function() { + return { + value: this.props.value + }; + }, + + render: function() { + var options = { + autoCloseBrackets: true, + gutters: ['CodeMirror-lint-markers'], + lint: true, + linter: jshint, + lineNumbers: true, + lineWrapping: true, + mode: 'javascript', + matchBrackets: true, + runnable: true, + scrollbarStyle: 'null', + theme: 'monokai', + textAreaClassName: 'hide-textarea', + value: this.state.value, + onChange: e => { + this.setState({ value: e.target.value}); + if (typeof this.props.onValueChange === 'function') { + this.props.onValueChange(e.target.value); + } + } + }; + + var config = { + setSize: ['100%', 'auto'], + extraKeys: { + Tab: function(cm) { + debug('tab pressed'); + if (cm.somethingSelected()) { + cm.indentSelection('add'); + } else { + var spaces = new Array(cm.getOption('indentUnit') + 1).join(' '); + cm.replaceSelection(spaces); + } + }, + 'Shift-Tab': function(cm) { + debug('shift-tab pressed'); + if (cm.somethingSelected()) { + cm.indentSelection('subtract'); + } else { + var spaces = new Array(cm.getOption('indentUnit') + 1).join(' '); + cm.replaceSelection(spaces); + } + }, + 'Ctrl-Enter': function() { + debug('C-enter pressed'); + // execute bonfire action + return false; + } + } + }; + + return ( +
+
+
+ +
+
+
+ ); + } +}); + +module.exports = Editor; diff --git a/common/screens/editor/index.js b/common/screens/editor/index.js new file mode 100644 index 0000000000..5e431fabff --- /dev/null +++ b/common/screens/editor/index.js @@ -0,0 +1 @@ +module.exports = require('./Editor.jsx'); diff --git a/common/screens/footer/Footer.jsx b/common/screens/footer/Footer.jsx new file mode 100644 index 0000000000..5dd5e1b296 --- /dev/null +++ b/common/screens/footer/Footer.jsx @@ -0,0 +1,106 @@ +var React = require('react'); + +var Footer = React.createClass({ + render: function() { + return ( +
+
+ +  Blog   + + +  Twitch   + + +  Github   + + +  Twitter   + + +  Facebook   + + +  About   + + +  Privacy   + +
+
+ + + Free Code Camp\'s Blog + + + + + Free Code Camp Live Pair Programming on Twitch.tv + + + + + Free Code Camp on GitHub + + + + + Free Code Camp on Twitter + + + + + Free Code Camp on Facebook + + + + + About Free Code Camp + + + + + Free Code Camp's Privacy Policy + + +
+
+ ); + } +}); + +module.exports = Footer; diff --git a/common/screens/footer/index.js b/common/screens/footer/index.js new file mode 100644 index 0000000000..286ef5f0fb --- /dev/null +++ b/common/screens/footer/index.js @@ -0,0 +1 @@ +module.exports = require('./Footer.jsx'); diff --git a/common/screens/nav/Nav.jsx b/common/screens/nav/Nav.jsx new file mode 100644 index 0000000000..52a9a8728b --- /dev/null +++ b/common/screens/nav/Nav.jsx @@ -0,0 +1,81 @@ +var React = require('react'), + bootStrap = require('react-bootstrap'), + Navbar = bootStrap.Navbar, + Nav = bootStrap.Nav, + NavItem = bootStrap.NavItem, + NavItemFCC = require('./NavItem.jsx'); + +var NavBarComp = React.createClass({ + + propTypes: { signedIn: React.PropTypes.bool }, + + getDefaultProps: function() { + return { signedIn: false }; + }, + + _renderBrand: function() { + var fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg'; + return ( + + learn to code javascript at Free Code Camp logo + + ); + }, + + _renderSignin: function() { + if (this.props.signedIn) { + return ( + + Show Picture + + ); + } else { + return ( + + Sign In + + ); + } + }, + + render: function() { + + return ( + + + + ); + } +}); +module.exports = NavBarComp; diff --git a/common/screens/nav/NavItem.jsx b/common/screens/nav/NavItem.jsx new file mode 100644 index 0000000000..88c4f363a9 --- /dev/null +++ b/common/screens/nav/NavItem.jsx @@ -0,0 +1,66 @@ +var React = require('react/addons'); +var joinClasses = require('react-bootstrap/lib/utils/joinClasses'); +var classSet = React.addons.classSet; +var BootstrapMixin = require('react-bootstrap').BootstrapMixin; + +var NavItem = React.createClass({ + mixins: [BootstrapMixin], + + propTypes: { + onSelect: React.PropTypes.func, + active: React.PropTypes.bool, + disabled: React.PropTypes.bool, + href: React.PropTypes.string, + title: React.PropTypes.string, + eventKey: React.PropTypes.any, + target: React.PropTypes.string + }, + + getDefaultProps: function () { + return { + href: '#' + }; + }, + + render: function () { + var { + disabled, + active, + href, + title, + target, + children, + } = this.props, + props = this.props, + classes = { + 'active': active, + 'disabled': disabled + }; + + return ( +
  • + + { children } + +
  • + ); + }, + + handleClick: function (e) { + if (this.props.onSelect) { + e.preventDefault(); + + if (!this.props.disabled) { + this.props.onSelect(this.props.eventKey, this.props.href, this.props.target); + } + } + } +}); + +module.exports = NavItem; diff --git a/common/screens/nav/index.js b/common/screens/nav/index.js new file mode 100644 index 0000000000..cdf087548a --- /dev/null +++ b/common/screens/nav/index.js @@ -0,0 +1 @@ +module.exports = require('./Nav.jsx'); diff --git a/package.json b/package.json index 81dbf86f39..e75a2f2a1d 100644 --- a/package.json +++ b/package.json @@ -57,27 +57,28 @@ "less": "~1.7.5", "less-middleware": "~2.0.1", "lodash": "~2.4.1", + "loopback": "^2.18.0", + "loopback-boot": "^2.8.0", + "loopback-component-passport": "^1.3.1", + "loopback-connector-mongodb": "^1.10.0", "lusca": "~1.0.2", "method-override": "~2.3.0", "moment": "~2.10.2", - "mongodb": "~1.4.33", - "mongoose": "~4.0.1", - "mongoose-long": "0.0.2", + "mongodb": "^2.0.33", "morgan": "~1.5.0", "node-slack": "0.0.7", "nodemailer": "~1.3.0", - "passport": "~0.2.1", - "passport-facebook": "~1.0.3", - "passport-github": "~0.1.5", - "passport-google-oauth": "~0.1.5", - "passport-linkedin-oauth2": "~1.2.1", - "passport-local": "~1.0.0", - "passport-oauth": "~1.0.0", - "passport-twitter": "~1.0.2", + "passport-facebook": "^2.0.0", + "passport-google-oauth": "^0.2.0", + "passport-google-oauth2": "^0.1.6", + "passport-linkedin-oauth2": "^1.2.1", + "passport-local": "^1.0.0", + "passport-oauth": "^1.0.0", + "passport-twitter": "^1.0.3", "ramda": "~0.10.0", "request": "~2.53.0", + "rx": "^2.5.3", "sanitize-html": "~1.6.1", - "sitemap": "~0.7.4", "twit": "~1.1.20", "uglify-js": "~2.4.15", "validator": "~3.22.1", @@ -94,7 +95,7 @@ "gulp": "~3.8.8", "gulp-eslint": "~0.9.0", "gulp-inject": "~1.0.2", - "gulp-nodemon": "~1.0.4", + "gulp-nodemon": "^2.0.3", "mocha": "~2.0.1", "multiline": "~1.0.1", "supertest": "~0.15.0" diff --git a/seed_data/bonfireMDNlinks.js b/seed/bonfireMDNlinks.js similarity index 100% rename from seed_data/bonfireMDNlinks.js rename to seed/bonfireMDNlinks.js diff --git a/seed_data/challenge-migration.js b/seed/challenge-migration.js similarity index 100% rename from seed_data/challenge-migration.js rename to seed/challenge-migration.js diff --git a/seed_data/challengeMapping.json b/seed/challengeMapping.json similarity index 100% rename from seed_data/challengeMapping.json rename to seed/challengeMapping.json diff --git a/seed_data/challenges/advanced-bonfires.json b/seed/challenges/advanced-bonfires.json similarity index 100% rename from seed_data/challenges/advanced-bonfires.json rename to seed/challenges/advanced-bonfires.json diff --git a/seed_data/challenges/basejumps.json b/seed/challenges/basejumps.json similarity index 100% rename from seed_data/challenges/basejumps.json rename to seed/challenges/basejumps.json diff --git a/seed_data/challenges/basic-bonfires.json b/seed/challenges/basic-bonfires.json similarity index 100% rename from seed_data/challenges/basic-bonfires.json rename to seed/challenges/basic-bonfires.json diff --git a/seed_data/challenges/basic-html5-and-css.json b/seed/challenges/basic-html5-and-css.json similarity index 99% rename from seed_data/challenges/basic-html5-and-css.json rename to seed/challenges/basic-html5-and-css.json index 93b5215440..d2a8e1f64e 100644 --- a/seed_data/challenges/basic-html5-and-css.json +++ b/seed/challenges/basic-html5-and-css.json @@ -237,7 +237,7 @@ "name": "Waypoint: Fill in the Blank with Placeholder Text", "difficulty": 0.015, "description": [ - "Replace the text inside your p element with the first few words of the provided \"Ktty Ipsum\" text.", + "Replace the text inside your p element with the first few words of the provided \"Kitty Ipsum\" text.", "Web developers traditionally use \"Lorem Ipsum\" text as placeholder text. It's called \"Lorem Ipsum\" text because those are the first two words of a famous passage by Cicero of Ancient Rome.", "\"Lorem Ipsum\" text has been used as placeholder text by typesetters since the 16th century, and this tradition continues on the web.", "Well, 5 centuries is long enough. Since we're building a CatPhotoApp, let's use something called \"Kitty Ipsum\"!", diff --git a/seed_data/challenges/basic-javascript.json b/seed/challenges/basic-javascript.json similarity index 100% rename from seed_data/challenges/basic-javascript.json rename to seed/challenges/basic-javascript.json diff --git a/seed_data/challenges/bootstrap.json b/seed/challenges/bootstrap.json similarity index 100% rename from seed_data/challenges/bootstrap.json rename to seed/challenges/bootstrap.json diff --git a/seed_data/challenges/computer-science.json b/seed/challenges/computer-science.json similarity index 100% rename from seed_data/challenges/computer-science.json rename to seed/challenges/computer-science.json diff --git a/seed_data/challenges/full-stack-javascript.json b/seed/challenges/full-stack-javascript.json similarity index 100% rename from seed_data/challenges/full-stack-javascript.json rename to seed/challenges/full-stack-javascript.json diff --git a/seed_data/challenges/functional-programming.json b/seed/challenges/functional-programming.json similarity index 100% rename from seed_data/challenges/functional-programming.json rename to seed/challenges/functional-programming.json diff --git a/seed_data/challenges/get-set-for-free-code-camp.json b/seed/challenges/get-set-for-free-code-camp.json similarity index 100% rename from seed_data/challenges/get-set-for-free-code-camp.json rename to seed/challenges/get-set-for-free-code-camp.json diff --git a/seed_data/challenges/intermediate-bonfires.json b/seed/challenges/intermediate-bonfires.json similarity index 100% rename from seed_data/challenges/intermediate-bonfires.json rename to seed/challenges/intermediate-bonfires.json diff --git a/seed_data/challenges/jquery-ajax-and-json.json b/seed/challenges/jquery-ajax-and-json.json similarity index 100% rename from seed_data/challenges/jquery-ajax-and-json.json rename to seed/challenges/jquery-ajax-and-json.json diff --git a/seed_data/challenges/object-oriented-javascript.json b/seed/challenges/object-oriented-javascript.json similarity index 100% rename from seed_data/challenges/object-oriented-javascript.json rename to seed/challenges/object-oriented-javascript.json diff --git a/seed_data/challenges/ziplines.json b/seed/challenges/ziplines.json similarity index 100% rename from seed_data/challenges/ziplines.json rename to seed/challenges/ziplines.json diff --git a/seed_data/field-guides.json b/seed/field-guides.json similarity index 99% rename from seed_data/field-guides.json rename to seed/field-guides.json index de6edba981..d241f8858b 100644 --- a/seed_data/field-guides.json +++ b/seed/field-guides.json @@ -683,7 +683,7 @@ "
  • Fork the Free Code Camp repository and open seed_data/bonfires.json to become familiar with the format of our bonfires.
  • ", "
  • Regardless of your bonfire's difficulty, put it as the last bonfire in the JSON file. Change one of the numbers in the ID to ensure that your bonfire has a unique ID.
  • ", "
  • In the terminal, run node seed_data/seed.js. Run gulp. You should be able to navigate to your new bonfire in the challenge map. Whenever you make a change to bonfire.json, you'll need to reseed in order to see these changes in the browser.
  • ", - "
  • Solved your own Bonfire. Confirmed that your tests work as expected and that your instructions are sufficiently clear.
  • ", + "
  • Solve your own Bonfire. Confirm that your tests work as expected and that your instructions are sufficiently clear.
  • ", "
  • Submit a pull request to Free Code Camp's Staging branch and in the pull request body, link to a gist that has your algorithmic solution.
  • ", " ", "

    ", diff --git a/seed_data/future-jquery-ajax-json.json b/seed/future-jquery-ajax-json.json similarity index 100% rename from seed_data/future-jquery-ajax-json.json rename to seed/future-jquery-ajax-json.json diff --git a/seed/index.js b/seed/index.js new file mode 100644 index 0000000000..ddad9d678f --- /dev/null +++ b/seed/index.js @@ -0,0 +1,99 @@ +/* eslint-disable no-process-exit */ +require('dotenv').load(); +var fs = require('fs'), + path = require('path'), + app = require('../server/server'), + fieldGuides = require('./field-guides.json'), + nonprofits = require('./nonprofits.json'), + jobs = require('./jobs.json'); + +var Challenge = app.models.Challenge; +var FieldGuide = app.models.FieldGuide; +var Nonprofit = app.models.Nonprofit; +var Job = app.models.Job; +var counter = 0; +var challenges = fs.readdirSync(path.join(__dirname, '/challenges')); +var offerings = 3 + challenges.length; + +var CompletionMonitor = function() { + counter++; + console.log('call ' + counter); + + if (counter < offerings) { + return; + } else { + process.exit(0); + } +}; + +Challenge.destroyAll(function(err, info) { + if (err) { + console.err(err); + } else { + console.log('Deleted ', info); + } + challenges.forEach(function (file) { + Challenge.create( + require('./challenges/' + file).challenges, + function (err) { + if (err) { + console.log(err); + } else { + console.log('Successfully parsed %s', file); + CompletionMonitor(); + } + } + ); + }); +}); + +FieldGuide.destroyAll(function(err, info) { + if (err) { + console.error(err); + } else { + console.log('Deleted ', info); + } + FieldGuide.create(fieldGuides, function(err, data) { + if (err) { + console.log(err); + } else { + console.log('Saved ', data); + } + CompletionMonitor(); + console.log('field guides'); + }); +}); + +Nonprofit.destroyAll(function(err, info) { + if (err) { + console.error(err); + } else { + console.log('Deleted ', info); + } + Nonprofit.create(nonprofits, function(err, data) { + if (err) { + console.log(err); + } else { + console.log('Saved ', data); + } + CompletionMonitor(); + console.log('nonprofits'); + }); +}); + +Job.destroyAll(function(err, info) { + if (err) { + console.error(err); + } else { + console.log('Deleted ', info); + } + Job.create(jobs, function(err, data) { + if (err) { + console.log(err); + } else { + console.log('Saved ', data); + } + console.log('jobs'); + CompletionMonitor(); + }); +}); diff --git a/seed_data/jobs.json b/seed/jobs.json similarity index 100% rename from seed_data/jobs.json rename to seed/jobs.json diff --git a/seed_data/nonprofits.json b/seed/nonprofits.json similarity index 100% rename from seed_data/nonprofits.json rename to seed/nonprofits.json diff --git a/seed_data/storyCleanup.js b/seed/storyCleanup.js similarity index 100% rename from seed_data/storyCleanup.js rename to seed/storyCleanup.js diff --git a/seed_data/userMigration.js b/seed/userMigration.js similarity index 100% rename from seed_data/userMigration.js rename to seed/userMigration.js diff --git a/seed_data/seed.js b/seed_data/seed.js deleted file mode 100644 index 0221952d72..0000000000 --- a/seed_data/seed.js +++ /dev/null @@ -1,97 +0,0 @@ -require('dotenv').load(); -var Challenge = require('../models/Challenge.js'), - FieldGuide = require('../models/FieldGuide.js'), - Nonprofit = require('../models/Nonprofit.js'), - Job = require('../models/Job.js'), - mongoose = require('mongoose'), - secrets = require('../config/secrets'), - fieldGuides = require('./field-guides.json'), - nonprofits = require('./nonprofits.json'), - jobs = require('./jobs.json'), - fs = require('fs'); - -mongoose.connect(secrets.db); -var challenges = fs.readdirSync(__dirname + '/challenges'); - -var counter = 0; -var offerings = 3 + challenges.length; - -var CompletionMonitor = function() { - counter++; - console.log('call ' + counter); - - if (counter < offerings) { - return; - } else { - process.exit(0); - } -}; - -Challenge.remove({}, function(err, data) { - if (err) { - console.err(err); - } else { - console.log('Deleted ', data); - } - challenges.forEach(function (file) { - Challenge.create(require('./challenges/' + file).challenges, function (err, data) { - if (err) { - console.log(err); - } else { - console.log('Successfully parsed %s', file); - CompletionMonitor(); - } - }); - }); -}); - -FieldGuide.remove({}, function(err, data) { - if (err) { - console.error(err); - } else { - console.log('Deleted ', data); - } - FieldGuide.create(fieldGuides, function(err, data) { - if (err) { - console.log(err); - } else { - console.log('Saved ', data); - } - CompletionMonitor(); - }); - console.log('field guides'); -}); - -Nonprofit.remove({}, function(err, data) { - if (err) { - console.error(err); - } else { - console.log('Deleted ', data); - } - Nonprofit.create(nonprofits, function(err, data) { - if (err) { - console.log(err); - } else { - console.log('Saved ', data); - } - CompletionMonitor(); - }); - console.log('nonprofits'); -}); - -Job.remove({}, function(err, data) { - if (err) { - console.error(err); - } else { - console.log('Deleted ', data); - } - Job.create(jobs, function(err, data) { - if (err) { - console.log(err); - } else { - console.log('Saved ', data); - } - CompletionMonitor(); - }); - console.log('jobs'); -}); diff --git a/server/boot/authentication.js b/server/boot/authentication.js new file mode 100644 index 0000000000..3ccd6de22e --- /dev/null +++ b/server/boot/authentication.js @@ -0,0 +1,4 @@ +module.exports = function enableAuthentication(app) { + // enable authentication + app.enableAuth(); +}; diff --git a/server/boot/challenge.js b/server/boot/challenge.js index b2bb53142c..135523087b 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -31,33 +31,13 @@ */ var R = require('ramda'), - express = require('express'), - Challenge = require('../../common/models/Challenge'), - User = require('../../common/models/User'), - resources = require('../resources/resources'), - userMigration = require('../resources/middleware').userMigration, - MDNlinks = require('../../seed_data/bonfireMDNlinks'); + utils = require('../utils'), + userMigration = require('../utils/middleware').userMigration, + MDNlinks = require('../../seed/bonfireMDNlinks'); -var router = express.Router(); -var challengeMapWithNames = resources.getChallengeMapWithNames(); -var challengeMapWithIds = resources.getChallengeMapWithIds(); +var challengeMapWithNames = utils.getChallengeMapWithNames(); +var challengeMapWithIds = utils.getChallengeMapWithIds(); -router.get( - '/challenges/next-challenge', - userMigration, - returnNextChallenge -); - -router.get( - '/challenges/:challengeName', - userMigration, - returnIndividualChallenge -); - -router.get('/challenges/', userMigration, returnCurrentChallenge); -router.post('/completed-challenge/', completedChallenge); -router.post('/completed-zipline-or-basejump', completedZiplineOrBasejump); -router.post('/completed-bonfire', completedBonfire); function getMDNlinks(links) { // takes in an array of links, which are strings @@ -73,278 +53,302 @@ function getMDNlinks(links) { return populatedLinks; } -function returnNextChallenge(req, res, next) { - if (!req.user) { - return res.redirect('../challenges/learn-how-free-code-camp-works'); - } - var completed = req.user.completedChallenges.map(function (elem) { - return elem._id; - }); +module.exports = function(app) { + var router = app.loopback.Router(); + var Challenge = app.models.Challenge; + var User = app.models.User; - req.user.uncompletedChallenges = resources.allChallengeIds() - .filter(function (elem) { - if (completed.indexOf(elem) === -1) { - return elem; - } - }); + router.get( + '/challenges/next-challenge', + userMigration, + returnNextChallenge + ); - // find the user's current challenge and block - // look in that block and find the index of their current challenge - // if index + 1 < block.challenges.length - // serve index + 1 challenge - // otherwise increment block key and serve the first challenge in that block - // unless the next block is undefined, which means no next block - var nextChallengeName; + router.get( + '/challenges/:challengeName', + userMigration, + returnIndividualChallenge + ); - var challengeId = String(req.user.currentChallenge.challengeId); - var challengeBlock = req.user.currentChallenge.challengeBlock; - var indexOfChallenge = challengeMapWithIds[challengeBlock] - .indexOf(challengeId); + router.get('/challenges/', userMigration, returnCurrentChallenge); + router.post('/completed-challenge/', completedChallenge); + router.post('/completed-zipline-or-basejump', completedZiplineOrBasejump); + router.post('/completed-bonfire', completedBonfire); - if (indexOfChallenge + 1 - < challengeMapWithIds[challengeBlock].length) { - nextChallengeName = - challengeMapWithNames[challengeBlock][++indexOfChallenge]; - } else if (typeof challengeMapWithIds[++challengeBlock] !== 'undefined') { - nextChallengeName = R.head(challengeMapWithNames[challengeBlock]); - } else { - req.flash('errors', { - msg: 'It looks like you have finished all of our challenges.' + - ' Great job! Now on to helping nonprofits!' - }); - nextChallengeName = R.head(challengeMapWithNames[0].challenges); - } + app.use(router); - var nameString = nextChallengeName.trim() - .toLowerCase() - .replace(/\s/g, '-'); - - req.user.save(function(err) { - if (err) { - return next(err); + function returnNextChallenge(req, res, next) { + if (!req.user) { + return res.redirect('../challenges/learn-how-free-code-camp-works'); } - return res.redirect('../challenges/' + nameString); - }); -} - -function returnCurrentChallenge(req, res, next) { - if (!req.user) { - return res.redirect('../challenges/learn-how-free-code-camp-works'); - } - var completed = req.user.completedChallenges.map(function (elem) { - return elem._id; - }); - - req.user.uncompletedChallenges = resources.allChallengeIds() - .filter(function (elem) { - if (completed.indexOf(elem) === -1) { - return elem; - } + var completed = req.user.completedChallenges.map(function (elem) { + return elem._id; }); - if (!req.user.currentChallenge) { - req.user.currentChallenge = {}; - req.user.currentChallenge.challengeId = challengeMapWithIds['0'][0]; - req.user.currentChallenge.challengeName = challengeMapWithNames['0'][0]; - req.user.currentChallenge.challengeBlock = '0'; + + req.user.uncompletedChallenges = utils.allChallengeIds() + .filter(function (elem) { + if (completed.indexOf(elem) === -1) { + return elem; + } + }); + + // find the user's current challenge and block + // look in that block and find the index of their current challenge + // if index + 1 < block.challenges.length + // serve index + 1 challenge + // otherwise increment block key and serve the first challenge in that block + // unless the next block is undefined, which means no next block + var nextChallengeName; + + var challengeId = String(req.user.currentChallenge.challengeId); + var challengeBlock = req.user.currentChallenge.challengeBlock; + var indexOfChallenge = challengeMapWithIds[challengeBlock] + .indexOf(challengeId); + + if (indexOfChallenge + 1 + < challengeMapWithIds[challengeBlock].length) { + nextChallengeName = + challengeMapWithNames[challengeBlock][++indexOfChallenge]; + } else if (typeof challengeMapWithIds[++challengeBlock] !== 'undefined') { + nextChallengeName = R.head(challengeMapWithNames[challengeBlock]); + } else { + req.flash('errors', { + msg: 'It looks like you have finished all of our challenges.' + + ' Great job! Now on to helping nonprofits!' + }); + nextChallengeName = R.head(challengeMapWithNames[0].challenges); + } + + var nameString = nextChallengeName.trim() + .toLowerCase() + .replace(/\s/g, '-'); + req.user.save(function(err) { if (err) { return next(err); } + return res.redirect('../challenges/' + nameString); }); } - var nameString = req.user.currentChallenge.challengeName.trim() - .toLowerCase() - .replace(/\s/g, '-') - .replace(/[^a-z0-9\-\/.]/gi, ''); - req.user.save(function(err) { - if (err) { - return next(err); + + function returnCurrentChallenge(req, res, next) { + if (!req.user) { + return res.redirect('../challenges/learn-how-free-code-camp-works'); } - return res.redirect('../challenges/' + nameString); - }); -} - -function returnIndividualChallenge(req, res, next) { - var dashedName = req.params.challengeName; - - var challengeName = - (/^(bonfire|waypoint|zipline|basejump)/i).test(dashedName) ? - dashedName - .replace(/\-/g, ' ') - .split(' ') - .slice(1) - .join(' ') : - dashedName.replace(/\-/g, ' '); - - Challenge.find({'name': new RegExp(challengeName, 'i')}, - function(err, challengeFromMongo) { - if (err) { - return next(err); - } - // Handle not found - if (challengeFromMongo.length < 1) { - req.flash('errors', { - msg: '404: We couldn\'t find a challenge with that name. ' + - 'Please double check the name.' - }); - return res.redirect('/challenges'); - } - var challenge = challengeFromMongo.pop(); - // Redirect to full name if the user only entered a partial - var dashedNameFull = challenge.name - .toLowerCase() - .replace(/\s/g, '-') - .replace(/[^a-z0-9\-\.]/gi, ''); - if (dashedNameFull !== dashedName) { - return res.redirect('../challenges/' + dashedNameFull); - } else if (req.user) { - req.user.currentChallenge = { - challengeId: challenge._id, - challengeName: challenge.name, - challengeBlock: R.head(R.flatten(Object.keys(challengeMapWithIds). - map(function (key) { - return challengeMapWithIds[key] - .filter(function (elem) { - return String(elem) === String(challenge._id); - }).map(function () { - return key; - }); - }) - )) - }; - } - - var challengeType = { - 0: function() { - res.render('coursewares/showHTML', { - title: challenge.name, - dashedName: dashedName, - name: challenge.name, - brief: challenge.description[0], - details: challenge.description.slice(1), - tests: challenge.tests, - challengeSeed: challenge.challengeSeed, - verb: resources.randomVerb(), - phrase: resources.randomPhrase(), - compliment: resources.randomCompliment(), - challengeId: challenge._id, - environment: resources.whichEnvironment(), - challengeType: challenge.challengeType - }); - }, - - 1: function() { - res.render('coursewares/showJS', { - title: challenge.name, - dashedName: dashedName, - name: challenge.name, - brief: challenge.description[0], - details: challenge.description.slice(1), - tests: challenge.tests, - challengeSeed: challenge.challengeSeed, - verb: resources.randomVerb(), - phrase: resources.randomPhrase(), - compliment: resources.randomCompliment(), - challengeId: challenge._id, - challengeType: challenge.challengeType - }); - }, - - 2: function() { - res.render('coursewares/showVideo', { - title: challenge.name, - dashedName: dashedName, - name: challenge.name, - details: challenge.description, - tests: challenge.tests, - video: challenge.challengeSeed[0], - verb: resources.randomVerb(), - phrase: resources.randomPhrase(), - compliment: resources.randomCompliment(), - challengeId: challenge._id, - challengeType: challenge.challengeType - }); - }, - - 3: function() { - res.render('coursewares/showZiplineOrBasejump', { - title: challenge.name, - dashedName: dashedName, - name: challenge.name, - details: challenge.description, - video: challenge.challengeSeed[0], - verb: resources.randomVerb(), - phrase: resources.randomPhrase(), - compliment: resources.randomCompliment(), - challengeId: challenge._id, - challengeType: challenge.challengeType - }); - }, - - 4: function() { - res.render('coursewares/showZiplineOrBasejump', { - title: challenge.name, - dashedName: dashedName, - name: challenge.name, - details: challenge.description, - video: challenge.challengeSeed[0], - verb: resources.randomVerb(), - phrase: resources.randomPhrase(), - compliment: resources.randomCompliment(), - challengeId: challenge._id, - challengeType: challenge.challengeType - }); - }, - - 5: function() { - res.render('coursewares/showBonfire', { - completedWith: null, - title: challenge.name, - dashedName: dashedName, - name: challenge.name, - difficulty: Math.floor(+challenge.difficulty), - brief: challenge.description.shift(), - details: challenge.description, - tests: challenge.tests, - challengeSeed: challenge.challengeSeed, - verb: resources.randomVerb(), - phrase: resources.randomPhrase(), - compliment: resources.randomCompliment(), - bonfires: challenge, - challengeId: challenge._id, - MDNkeys: challenge.MDNlinks, - MDNlinks: getMDNlinks(challenge.MDNlinks), - challengeType: challenge.challengeType - }); - } - }; - if (req.user) { - req.user.save(function (err) { - if (err) { - return next(err); - } - return challengeType[challenge.challengeType](); - }); - } else { - return challengeType[challenge.challengeType](); - } + var completed = req.user.completedChallenges.map(function (elem) { + return elem._id; }); -} -function completedBonfire(req, res, next) { - var isCompletedWith = req.body.challengeInfo.completedWith || ''; - var isCompletedDate = Math.round(+new Date()); - var challengeId = req.body.challengeInfo.challengeId; - var isSolution = req.body.challengeInfo.solution; - var challengeName = req.body.challengeInfo.challengeName; - - if (isCompletedWith) { - var paired = User.find({'profile.username': isCompletedWith.toLowerCase()}) - .limit(1); - paired.exec(function (err, pairedWith) { + req.user.uncompletedChallenges = utils.allChallengeIds() + .filter(function (elem) { + if (completed.indexOf(elem) === -1) { + return elem; + } + }); + if (!req.user.currentChallenge) { + req.user.currentChallenge = {}; + req.user.currentChallenge.challengeId = challengeMapWithIds['0'][0]; + req.user.currentChallenge.challengeName = challengeMapWithNames['0'][0]; + req.user.currentChallenge.challengeBlock = '0'; + req.user.save(function(err) { + if (err) { + return next(err); + } + }); + } + var nameString = req.user.currentChallenge.challengeName.trim() + .toLowerCase() + .replace(/\s/g, '-') + .replace(/[^a-z0-9\-\/.]/gi, ''); + req.user.save(function(err) { if (err) { return next(err); - } else { + } + return res.redirect('../challenges/' + nameString); + }); + } + + function returnIndividualChallenge(req, res, next) { + var dashedName = req.params.challengeName; + + var challengeName = + (/^(bonfire|waypoint|zipline|basejump)/i).test(dashedName) ? + dashedName + .replace(/\-/g, ' ') + .split(' ') + .slice(1) + .join(' ') : + dashedName.replace(/\-/g, ' '); + + Challenge.find( + { where: { name: new RegExp(challengeName, 'i') } }, + function(err, challengeFromMongo) { + if (err) { return next(err); } + + // Handle not found + if (challengeFromMongo.length < 1) { + req.flash('errors', { + msg: '404: We couldn\'t find a challenge with that name. ' + + 'Please double check the name.' + }); + return res.redirect('/challenges'); + } + var challenge = challengeFromMongo.pop(); + // Redirect to full name if the user only entered a partial + var dashedNameFull = challenge.name + .toLowerCase() + .replace(/\s/g, '-') + .replace(/[^a-z0-9\-\.]/gi, ''); + if (dashedNameFull !== dashedName) { + return res.redirect('../challenges/' + dashedNameFull); + } else if (req.user) { + req.user.currentChallenge = { + challengeId: challenge._id, + challengeName: challenge.name, + challengeBlock: R.head(R.flatten(Object.keys(challengeMapWithIds). + map(function (key) { + return challengeMapWithIds[key] + .filter(function (elem) { + return String(elem) === String(challenge._id); + }).map(function () { + return key; + }); + }) + )) + }; + } + + var challengeType = { + 0: function() { + res.render('coursewares/showHTML', { + title: challenge.name, + dashedName: dashedName, + name: challenge.name, + brief: challenge.description[0], + details: challenge.description.slice(1), + tests: challenge.tests, + challengeSeed: challenge.challengeSeed, + verb: utils.randomVerb(), + phrase: utils.randomPhrase(), + compliment: utils.randomCompliment(), + challengeId: challenge._id, + environment: utils.whichEnvironment(), + challengeType: challenge.challengeType + }); + }, + + 1: function() { + res.render('coursewares/showJS', { + title: challenge.name, + dashedName: dashedName, + name: challenge.name, + brief: challenge.description[0], + details: challenge.description.slice(1), + tests: challenge.tests, + challengeSeed: challenge.challengeSeed, + verb: utils.randomVerb(), + phrase: utils.randomPhrase(), + compliment: utils.randomCompliment(), + challengeId: challenge._id, + challengeType: challenge.challengeType + }); + }, + + 2: function() { + res.render('coursewares/showVideo', { + title: challenge.name, + dashedName: dashedName, + name: challenge.name, + details: challenge.description, + tests: challenge.tests, + video: challenge.challengeSeed[0], + verb: utils.randomVerb(), + phrase: utils.randomPhrase(), + compliment: utils.randomCompliment(), + challengeId: challenge._id, + challengeType: challenge.challengeType + }); + }, + + 3: function() { + res.render('coursewares/showZiplineOrBasejump', { + title: challenge.name, + dashedName: dashedName, + name: challenge.name, + details: challenge.description, + video: challenge.challengeSeed[0], + verb: utils.randomVerb(), + phrase: utils.randomPhrase(), + compliment: utils.randomCompliment(), + challengeId: challenge._id, + challengeType: challenge.challengeType + }); + }, + + 4: function() { + res.render('coursewares/showZiplineOrBasejump', { + title: challenge.name, + dashedName: dashedName, + name: challenge.name, + details: challenge.description, + video: challenge.challengeSeed[0], + verb: utils.randomVerb(), + phrase: utils.randomPhrase(), + compliment: utils.randomCompliment(), + challengeId: challenge._id, + challengeType: challenge.challengeType + }); + }, + + 5: function() { + res.render('coursewares/showBonfire', { + completedWith: null, + title: challenge.name, + dashedName: dashedName, + name: challenge.name, + difficulty: Math.floor(+challenge.difficulty), + brief: challenge.description.shift(), + details: challenge.description, + tests: challenge.tests, + challengeSeed: challenge.challengeSeed, + verb: utils.randomVerb(), + phrase: utils.randomPhrase(), + compliment: utils.randomCompliment(), + bonfires: challenge, + challengeId: challenge._id, + MDNkeys: challenge.MDNlinks, + MDNlinks: getMDNlinks(challenge.MDNlinks), + challengeType: challenge.challengeType + }); + } + }; + if (req.user) { + req.user.save(function (err) { + if (err) { + return next(err); + } + return challengeType[challenge.challengeType](); + }); + } else { + return challengeType[challenge.challengeType](); + } + }); + } + + function completedBonfire(req, res, next) { + var isCompletedWith = req.body.challengeInfo.completedWith || ''; + var isCompletedDate = Math.round(+new Date()); + var challengeId = req.body.challengeInfo.challengeId; + var isSolution = req.body.challengeInfo.solution; + var challengeName = req.body.challengeInfo.challengeName; + + if (isCompletedWith) { + User.find({ + where: { 'profile.username': isCompletedWith.toLowerCase() }, + limit: 1 + }, function (err, pairedWith) { + if (err) { return next(err); } + var index = req.user.uncompletedChallenges.indexOf(challengeId); if (index > -1) { req.user.progressTimestamps.push(Date.now() || 0); @@ -379,20 +383,18 @@ function completedBonfire(req, res, next) { }); } // User said they paired, but pair wasn't found - req.user.completedChallenges.push({ - _id: challengeId, - name: challengeName, - completedWith: null, - completedDate: isCompletedDate, - solution: isSolution, - challengeType: 5 - }); - + req.user.completedChallenges.push({ + _id: challengeId, + name: challengeName, + completedWith: null, + completedDate: isCompletedDate, + solution: isSolution, + challengeType: 5 + }); req.user.save(function (err, user) { - if (err) { - return next(err); - } + if (err) { return next(err); } + if (pairedWith) { pairedWith.save(function (err, paired) { if (err) { @@ -406,21 +408,47 @@ function completedBonfire(req, res, next) { res.send(true); } }); + }); + } else { + req.user.completedChallenges.push({ + _id: challengeId, + name: challengeName, + completedWith: null, + completedDate: isCompletedDate, + solution: isSolution, + challengeType: 5 + }); + + var index = req.user.uncompletedChallenges.indexOf(challengeId); + if (index > -1) { + + req.user.progressTimestamps.push(Date.now() || 0); + req.user.uncompletedChallenges.splice(index, 1); } - }); - } else { + + req.user.save(function (err) { + if (err) { return next(err); } + res.send(true); + }); + } + } + + function completedChallenge(req, res, next) { + + var isCompletedDate = Math.round(+new Date()); + var challengeId = req.body.challengeInfo.challengeId; + req.user.completedChallenges.push({ _id: challengeId, - name: challengeName, - completedWith: null, completedDate: isCompletedDate, - solution: isSolution, - challengeType: 5 + name: req.body.challengeInfo.challengeName, + solution: null, + githubLink: null, + verified: true }); - var index = req.user.uncompletedChallenges.indexOf(challengeId); - if (index > -1) { + if (index > -1) { req.user.progressTimestamps.push(Date.now() || 0); req.user.uncompletedChallenges.splice(index, 1); } @@ -429,70 +457,36 @@ function completedBonfire(req, res, next) { if (err) { return next(err); } - // NOTE(berks): Under certain conditions the res is never ended if (user) { - res.send(true); + res.sendStatus(200); } }); } -} -function completedChallenge(req, res, next) { + function completedZiplineOrBasejump(req, res, next) { - var isCompletedDate = Math.round(+new Date()); - var challengeId = req.body.challengeInfo.challengeId; - - req.user.completedChallenges.push({ - _id: challengeId, - completedDate: isCompletedDate, - name: req.body.challengeInfo.challengeName, - solution: null, - githubLink: null, - verified: true - }); - var index = req.user.uncompletedChallenges.indexOf(challengeId); - - if (index > -1) { - req.user.progressTimestamps.push(Date.now() || 0); - req.user.uncompletedChallenges.splice(index, 1); - } - - req.user.save(function (err, user) { - if (err) { - return next(err); + var isCompletedWith = req.body.challengeInfo.completedWith || false; + var isCompletedDate = Math.round(+new Date()); + var challengeId = req.body.challengeInfo.challengeId; + var solutionLink = req.body.challengeInfo.publicURL; + var githubLink = req.body.challengeInfo.challengeType === '4' + ? req.body.challengeInfo.githubURL : true; + var challengeType = req.body.challengeInfo.challengeType === '4' ? + 4 : 3; + if (!solutionLink || !githubLink) { + req.flash('errors', { + msg: 'You haven\'t supplied the necessary URLs for us to inspect ' + + 'your work.' + }); + return res.sendStatus(403); } - if (user) { - res.sendStatus(200); - } - }); -} -function completedZiplineOrBasejump(req, res, next) { - - var isCompletedWith = req.body.challengeInfo.completedWith || false; - var isCompletedDate = Math.round(+new Date()); - var challengeId = req.body.challengeInfo.challengeId; - var solutionLink = req.body.challengeInfo.publicURL; - var githubLink = req.body.challengeInfo.challengeType === '4' - ? req.body.challengeInfo.githubURL : true; - var challengeType = req.body.challengeInfo.challengeType === '4' ? - 4 : 3; - if (!solutionLink || !githubLink) { - req.flash('errors', { - msg: 'You haven\'t supplied the necessary URLs for us to inspect ' + - 'your work.' - }); - return res.sendStatus(403); - } - - if (isCompletedWith) { - var paired = User.find({'profile.username': isCompletedWith.toLowerCase()}) - .limit(1); - - paired.exec(function (err, pairedWithFromMongo) { - if (err) { - return next(err); - } else { + if (isCompletedWith) { + User.find({ + where: { 'profile.username': isCompletedWith.toLowerCase() }, + limit: 1 + }, function (err, pairedWithFromMongo) { + if (err) { return next(err); } var index = req.user.uncompletedChallenges.indexOf(challengeId); if (index > -1) { req.user.progressTimestamps.push(Date.now() || 0); @@ -512,9 +506,7 @@ function completedZiplineOrBasejump(req, res, next) { }); req.user.save(function (err, user) { - if (err) { - return next(err); - } + if (err) { return next(err); } if (req.user._id.toString() === pairedWith._id.toString()) { return res.sendStatus(200); @@ -545,37 +537,36 @@ function completedZiplineOrBasejump(req, res, next) { } }); }); + }); + } else { + + req.user.completedChallenges.push({ + _id: challengeId, + name: req.body.challengeInfo.challengeName, + completedWith: null, + completedDate: isCompletedDate, + solution: solutionLink, + githubLink: githubLink, + challengeType: challengeType, + verified: false + }); + + var index = req.user.uncompletedChallenges.indexOf(challengeId); + if (index > -1) { + req.user.progressTimestamps.push(Date.now() || 0); + req.user.uncompletedChallenges.splice(index, 1); } - }); - } else { - req.user.completedChallenges.push({ - _id: challengeId, - name: req.body.challengeInfo.challengeName, - completedWith: null, - completedDate: isCompletedDate, - solution: solutionLink, - githubLink: githubLink, - challengeType: challengeType, - verified: false - }); - - var index = req.user.uncompletedChallenges.indexOf(challengeId); - if (index > -1) { - req.user.progressTimestamps.push(Date.now() || 0); - req.user.uncompletedChallenges.splice(index, 1); + req.user.save(function (err, user) { + if (err) { + return next(err); + } + // NOTE(berks): under certain conditions this will not close + // the response. + if (user) { + return res.sendStatus(200); + } + }); } - - req.user.save(function (err, user) { - if (err) { - return next(err); - } - // NOTE(berks): under certain conditions this will not close the response. - if (user) { - return res.sendStatus(200); - } - }); } -} - -module.exports = router; +}; diff --git a/server/boot/challengeMap.js b/server/boot/challengeMap.js index 8d8bb727c9..42261ac378 100644 --- a/server/boot/challengeMap.js +++ b/server/boot/challengeMap.js @@ -1,65 +1,65 @@ var R = require('ramda'), - express = require('express'), // debug = require('debug')('freecc:cntr:challengeMap'), - User = require('../../common/models/User'), - resources = require('./../resources/resources'), - middleware = require('../resources/middleware'), - router = express.Router(); + utils = require('./../utils'), + middleware = require('../utils/middleware'); -router.get('/map', middleware.userMigration, challengeMap); -router.get('/learn-to-code', function(req, res) { - res.redirect(301, '/map'); -}); +module.exports = function(app) { + var User = app.models.User; + var router = app.loopback.Router(); -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); - - Object.keys(challengeList).forEach(function(key) { - challengeList[key].completed = challengeList[key] - .challenges.filter(function(elem) { - return completedChallengeList.indexOf(elem._id) > -1; - }); + 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 numberWithCommas(x) { - return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); - } + app.use(router); - 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)); + function challengeMap(req, res, next) { + var completedList = []; - User.count({}, function (err, camperCount) { - if (err) { - return next(err); + if (req.user) { + completedList = req.user.completedChallenges; } - 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; + var noDuplicatedChallenges = R.uniq(completedList); + + var completedChallengeList = noDuplicatedChallenges + .map(function(challenge) { + return challenge._id; + }); + var challengeList = utils. + getChallengeMapForDisplay(completedChallengeList); + + Object.keys(challengeList).forEach(function(key) { + challengeList[key].completed = challengeList[key] + .challenges.filter(function(elem) { + return completedChallengeList.indexOf(elem._id) > -1; + }); + }); + + 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/server/boot/explorer.js b/server/boot/explorer.js new file mode 100644 index 0000000000..7461836f63 --- /dev/null +++ b/server/boot/explorer.js @@ -0,0 +1,27 @@ +module.exports = function mountLoopBackExplorer(app) { + var explorer; + try { + explorer = require('loopback-explorer'); + } catch(err) { + // Print the message only when the app was started via `app.listen()`. + // Do not print any message when the project is used as a component. + app.once('started', function() { + console.log( + 'Run `npm install loopback-explorer` to enable the LoopBack explorer' + ); + }); + return; + } + + var restApiRoot = app.get('restApiRoot'); + + var explorerApp = explorer(app, { basePath: restApiRoot }); + app.use('/explorer', explorerApp); + app.once('started', function() { + var baseUrl = app.get('url').replace(/\/$/, ''); + // express 4.x (loopback 2.x) uses `mountpath` + // express 3.x (loopback 1.x) uses `route` + var explorerPath = explorerApp.mountpath || explorerApp.route; + console.log('Browse your REST API at %s%s', baseUrl, explorerPath); + }); +}; diff --git a/server/boot/fieldGuide.js b/server/boot/fieldGuide.js index 7497a4440e..8cebbe461b 100644 --- a/server/boot/fieldGuide.js +++ b/server/boot/fieldGuide.js @@ -1,123 +1,128 @@ var R = require('ramda'), - express = require('express'), + // Rx = require('rx'), // debug = require('debug')('freecc:fieldguides'), - FieldGuide = require('../../common/models/FieldGuide'), - resources = require('../resources/resources'); + utils = require('../utils'); -var router = express.Router(); +module.exports = function(app) { + var router = app.loopback.Router(); + var FieldGuide = app.models.FieldGuide; -router.get('/field-guide/all-articles', showAllFieldGuides); -router.get('/field-guide/:fieldGuideName', returnIndividualFieldGuide); -router.get('/field-guide/', returnNextFieldGuide); -router.post('/completed-field-guide/', completedFieldGuide); + router.get('/field-guide/all-articles', showAllFieldGuides); + router.get('/field-guide/:fieldGuideName', returnIndividualFieldGuide); + router.get('/field-guide/', returnNextFieldGuide); + router.post('/completed-field-guide/', completedFieldGuide); -function returnIndividualFieldGuide(req, res, next) { - var dashedName = req.params.fieldGuideName; - if (req.user) { - var completed = req.user.completedFieldGuides; + app.use(router); - var uncompletedFieldGuides = resources.allFieldGuideIds() - .filter(function (elem) { - if (completed.indexOf(elem) === -1) { - return elem; - } + function returnIndividualFieldGuide(req, res, next) { + var dashedName = req.params.fieldGuideName; + if (req.user) { + var completed = req.user.completedFieldGuides; + + var uncompletedFieldGuides = utils.allFieldGuideIds() + .filter(function (elem) { + if (completed.indexOf(elem) === -1) { + return elem; + } + }); + req.user.uncompletedFieldGuides = uncompletedFieldGuides; + // TODO(berks): handle callback properly + req.user.save(function(err) { + if (err) { return next(err); } }); - req.user.uncompletedFieldGuides = uncompletedFieldGuides; - // TODO(berks): handle callback properly - req.user.save(); + } + + // NOTE(berks): loopback might have issue with regex here. + FieldGuide.find( + { dashedName: new RegExp(dashedName, 'i') }, + function(err, fieldGuideFromMongo) { + if (err) { + return next(err); + } + + if (fieldGuideFromMongo.length < 1) { + req.flash('errors', { + msg: "404: We couldn't find a field guide entry with that name. " + + 'Please double check the name.' + }); + + return res.redirect('/field-guide'); + } + + var fieldGuide = R.head(fieldGuideFromMongo); + fieldGuide.name.toLowerCase().replace(/\s/g, '-').replace(/\?/g, ''); + + if (fieldGuide.dashedName !== dashedName) { + return res.redirect('../field-guide/' + fieldGuide.dashedName); + } + res.render('field-guide/show', { + title: fieldGuide.name, + fieldGuideId: fieldGuide._id, + description: fieldGuide.description.join('') + }); + } + ); } - FieldGuide.find( - { dashedName: new RegExp(dashedName, 'i') }, - function(err, fieldGuideFromMongo) { + function showAllFieldGuides(req, res) { + var allFieldGuideNamesAndIds = utils.allFieldGuideNamesAndIds(); + + var completedFieldGuides = []; + if (req.user && req.user.completedFieldGuides) { + completedFieldGuides = req.user.completedFieldGuides; + } + res.render('field-guide/all-articles', { + allFieldGuideNamesAndIds: allFieldGuideNamesAndIds, + completedFieldGuides: completedFieldGuides + }); + } + + function returnNextFieldGuide(req, res, next) { + if (!req.user) { + return res.redirect('/field-guide/how-do-i-use-this-guide'); + } + + var displayedFieldGuides = + FieldGuide.find({'_id': req.user.uncompletedFieldGuides[0]}); + + displayedFieldGuides.exec(function(err, fieldGuide) { + if (err) { return next(err); } + fieldGuide = fieldGuide.pop(); + + if (typeof fieldGuide === 'undefined') { + if (req.user.completedFieldGuides.length > 0) { + req.flash('success', { + msg: [ + "You've read all our current Field Guide entries. You can ", + 'contribute to our Field Guide ', + "here." + ].join('') + }); + } + return res.redirect('../field-guide/how-do-i-use-this-guide'); + } + var nameString = fieldGuide.name.toLowerCase().replace(/\s/g, '-'); + return res.redirect('../field-guide/' + nameString); + }); + } + + function completedFieldGuide(req, res, next) { + var fieldGuideId = req.body.fieldGuideInfo.fieldGuideId; + + req.user.completedFieldGuides.push(fieldGuideId); + + var index = req.user.uncompletedFieldGuides.indexOf(fieldGuideId); + if (index > -1) { + req.user.progressTimestamps.push(Date.now()); + req.user.uncompletedFieldGuides.splice(index, 1); + } + + req.user.save(function (err) { if (err) { return next(err); } - - if (fieldGuideFromMongo.length < 1) { - req.flash('errors', { - msg: "404: We couldn't find a field guide entry with that name. " + - 'Please double check the name.' - }); - - return res.redirect('/field-guide'); - } - - var fieldGuide = R.head(fieldGuideFromMongo); - fieldGuide.name.toLowerCase().replace(/\s/g, '-').replace(/\?/g, ''); - - if (fieldGuide.dashedName !== dashedName) { - return res.redirect('../field-guide/' + fieldGuide.dashedName); - } - res.render('field-guide/show', { - title: fieldGuide.name, - fieldGuideId: fieldGuide._id, - description: fieldGuide.description.join('') - }); - } - ); -} - -function showAllFieldGuides(req, res) { - var allFieldGuideNamesAndIds = resources.allFieldGuideNamesAndIds(); - - var completedFieldGuides = []; - if (req.user && req.user.completedFieldGuides) { - completedFieldGuides = req.user.completedFieldGuides; + res.send(true); + }); } - res.render('field-guide/all-articles', { - allFieldGuideNamesAndIds: allFieldGuideNamesAndIds, - completedFieldGuides: completedFieldGuides - }); -} - -function returnNextFieldGuide(req, res, next) { - if (!req.user) { - return res.redirect('/field-guide/how-do-i-use-this-guide'); - } - - var displayedFieldGuides = - FieldGuide.find({'_id': req.user.uncompletedFieldGuides[0]}); - - displayedFieldGuides.exec(function(err, fieldGuide) { - if (err) { return next(err); } - fieldGuide = fieldGuide.pop(); - - if (typeof fieldGuide === 'undefined') { - if (req.user.completedFieldGuides.length > 0) { - req.flash('success', { - msg: [ - "You've read all our current Field Guide entries. You can ", - 'contribute to our Field Guide ', - "here." - ].join('') - }); - } - return res.redirect('../field-guide/how-do-i-use-this-guide'); - } - var nameString = fieldGuide.name.toLowerCase().replace(/\s/g, '-'); - return res.redirect('../field-guide/' + nameString); - }); -} - -function completedFieldGuide(req, res, next) { - var fieldGuideId = req.body.fieldGuideInfo.fieldGuideId; - - req.user.completedFieldGuides.push(fieldGuideId); - - var index = req.user.uncompletedFieldGuides.indexOf(fieldGuideId); - if (index > -1) { - req.user.progressTimestamps.push(Date.now()); - req.user.uncompletedFieldGuides.splice(index, 1); - } - - req.user.save(function (err) { - if (err) { - return next(err); - } - res.send(true); - }); -} - -module.exports = router; +}; diff --git a/server/boot/home.js b/server/boot/home.js index 16cb3434a0..56bacf5c0f 100644 --- a/server/boot/home.js +++ b/server/boot/home.js @@ -1,22 +1,23 @@ -var express = require('express'); -var router = express.Router(); var message = 'Learn to Code JavaScript and get a Coding Job by Helping Nonprofits'; -router.get('/', index); +module.exports = function(app) { + var router = app.loopback.Router(); + 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'; + app.use(router); - req.user.save(function(err) { - if (err) { return next(err); } + 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'; + + req.user.save(function(err) { + if (err) { return next(err); } + res.render('home', { title: message }); + }); + } else { res.render('home', { title: message }); - }); - } else { - res.render('home', { title: message }); + } } -} - -module.exports = router; +}; diff --git a/server/boot/jobs.js b/server/boot/jobs.js index 5cba0c2f64..65d3feae99 100644 --- a/server/boot/jobs.js +++ b/server/boot/jobs.js @@ -1,18 +1,18 @@ -var express = require('express'); -var Job = require('../../common/models/Job'); -var router = express.Router(); +module.exports = function(app) { + var Job = app.models.Job; + var router = app.loopback.Router(); -router.get('/jobs', jobsDirectory); + router.get('/jobs', jobsDirectory); + app.use(router); -function jobsDirectory(req, res, next) { - Job.find({}, function(err, jobs) { - if (err) { return next(err); } + function jobsDirectory(req, res, next) { + Job.find({}, function(err, jobs) { + if (err) { return next(err); } - res.render('jobs/directory', { - title: 'Junior JavaScript Engineer Jobs', - jobs: jobs + res.render('jobs/directory', { + title: 'Junior JavaScript Engineer Jobs', + jobs: jobs + }); }); - }); -} - -module.exports = router; + } +}; diff --git a/server/boot/middlewares.js b/server/boot/middlewares.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/server/boot/nonprofits.js b/server/boot/nonprofits.js index 2fe25a6194..1ba62bf5b5 100644 --- a/server/boot/nonprofits.js +++ b/server/boot/nonprofits.js @@ -1,126 +1,130 @@ -var express = require('express'), - Nonprofit = require('../../common/models/Nonprofit'); -var router = express.Router(); +module.exports = function(app) { + var router = app.loopback.Router(); + var Nonprofit = app.models.Nonprofit; -router.get('/nonprofits/directory', nonprofitsDirectory); -router.get('/nonprofits/:nonprofitName', returnIndividualNonprofit); + router.get('/nonprofits/directory', nonprofitsDirectory); + router.get('/nonprofits/:nonprofitName', returnIndividualNonprofit); -function nonprofitsDirectory(req, res, next) { - Nonprofit.find({ estimatedHours: { $gt: 0 } }, function(err, nonprofits) { - if (err) { return next(err); } + app.use(router); - res.render('nonprofits/directory', { - title: 'Nonprofits we help', - nonprofits: nonprofits - }); - }); -} - -function returnIndividualNonprofit(req, res, next) { - var dashedName = req.params.nonprofitName; - var nonprofitName = dashedName.replace(/\-/g, ' '); - - Nonprofit.find( - { name: new RegExp(nonprofitName, 'i') }, - function(err, nonprofit) { - if (err) { - return next(err); - } - - if (nonprofit.length < 1) { - req.flash('errors', { - msg: "404: We couldn't find a nonprofit with that name. " + - 'Please double check the name.' - }); - - return res.redirect('/nonprofits'); - } - - nonprofit = nonprofit.pop(); - var dashedNameFull = nonprofit.name.toLowerCase().replace(/\s/g, '-'); - if (dashedNameFull !== dashedName) { - return res.redirect('../nonprofit/' + dashedNameFull); - } - var buttonActive = false; - if (req.user) { - if (req.user.uncompletedBonfires.length === 0) { - if (req.user.completedCoursewares.length > 63) { - var hasShownInterest = - nonprofit.interestedCampers.filter(function ( obj ) { - return obj.username === req.user.profile.username; - }); - - if (hasShownInterest.length === 0) { - buttonActive = true; - } - } - } - } - - res.render('nonprofits/show', { - dashedName: dashedNameFull, - title: nonprofit.name, - logoUrl: nonprofit.logoUrl, - estimatedHours: nonprofit.estimatedHours, - projectDescription: nonprofit.projectDescription, - - approvedOther: - nonprofit.approvedDeliverables.indexOf('other') > -1, - approvedWebsite: - nonprofit.approvedDeliverables.indexOf('website') > -1, - - approvedDonor: - nonprofit.approvedDeliverables.indexOf('donor') > -1, - approvedInventory: - nonprofit.approvedDeliverables.indexOf('inventory') > -1, - - approvedVolunteer: - nonprofit.approvedDeliverables.indexOf('volunteer') > -1, - approvedForm: - nonprofit.approvedDeliverables.indexOf('form') > -1, - - approvedCommunity: - nonprofit.approvedDeliverables.indexOf('community') > -1, - approvedELearning: - nonprofit.approvedDeliverables.indexOf('eLearning') > -1, - - websiteLink: nonprofit.websiteLink, - imageUrl: nonprofit.imageUrl, - whatDoesNonprofitDo: nonprofit.whatDoesNonprofitDo, - interestedCampers: nonprofit.interestedCampers, - assignedCampers: nonprofit.assignedCampers, - buttonActive: buttonActive, - currentStatus: nonprofit.currentStatus - }); - } - ); -} - -/* -function interestedInNonprofit(req, res, next) { - if (req.user) { - Nonprofit.findOne( - { name: new RegExp(req.params.nonprofitName.replace(/-/, ' '), 'i') }, - function(err, nonprofit) { + function nonprofitsDirectory(req, res, next) { + Nonprofit.find( + { where: { estimatedHours: { $gt: 0 } } }, + function(err, nonprofits) { if (err) { return next(err); } - nonprofit.interestedCampers.push({ - username: req.user.profile.username, - picture: req.user.profile.picture, - timeOfInterest: Date.now() - }); - nonprofit.save(function(err) { - if (err) { return next(err); } - req.flash('success', { - msg: 'Thanks for expressing interest in this nonprofit project! ' + - "We've added you to this project as an interested camper!" - }); - res.redirect('back'); + + res.render('nonprofits/directory', { + title: 'Nonprofits we help', + nonprofits: nonprofits }); } ); } -} -*/ -module.exports = router; + function returnIndividualNonprofit(req, res, next) { + var dashedName = req.params.nonprofitName; + var nonprofitName = dashedName.replace(/\-/g, ' '); + + Nonprofit.find( + { where: { name: new RegExp(nonprofitName, 'i') } }, + function(err, nonprofit) { + if (err) { + return next(err); + } + + if (nonprofit.length < 1) { + req.flash('errors', { + msg: "404: We couldn't find a nonprofit with that name. " + + 'Please double check the name.' + }); + + return res.redirect('/nonprofits'); + } + + nonprofit = nonprofit.pop(); + var dashedNameFull = nonprofit.name.toLowerCase().replace(/\s/g, '-'); + if (dashedNameFull !== dashedName) { + return res.redirect('../nonprofit/' + dashedNameFull); + } + var buttonActive = false; + if (req.user) { + if (req.user.uncompletedBonfires.length === 0) { + if (req.user.completedCoursewares.length > 63) { + var hasShownInterest = + nonprofit.interestedCampers.filter(function ( obj ) { + return obj.username === req.user.profile.username; + }); + + if (hasShownInterest.length === 0) { + buttonActive = true; + } + } + } + } + + res.render('nonprofits/show', { + dashedName: dashedNameFull, + title: nonprofit.name, + logoUrl: nonprofit.logoUrl, + estimatedHours: nonprofit.estimatedHours, + projectDescription: nonprofit.projectDescription, + + approvedOther: + nonprofit.approvedDeliverables.indexOf('other') > -1, + approvedWebsite: + nonprofit.approvedDeliverables.indexOf('website') > -1, + + approvedDonor: + nonprofit.approvedDeliverables.indexOf('donor') > -1, + approvedInventory: + nonprofit.approvedDeliverables.indexOf('inventory') > -1, + + approvedVolunteer: + nonprofit.approvedDeliverables.indexOf('volunteer') > -1, + approvedForm: + nonprofit.approvedDeliverables.indexOf('form') > -1, + + approvedCommunity: + nonprofit.approvedDeliverables.indexOf('community') > -1, + approvedELearning: + nonprofit.approvedDeliverables.indexOf('eLearning') > -1, + + websiteLink: nonprofit.websiteLink, + imageUrl: nonprofit.imageUrl, + whatDoesNonprofitDo: nonprofit.whatDoesNonprofitDo, + interestedCampers: nonprofit.interestedCampers, + assignedCampers: nonprofit.assignedCampers, + buttonActive: buttonActive, + currentStatus: nonprofit.currentStatus + }); + } + ); + } + + /* + function interestedInNonprofit(req, res, next) { + if (req.user) { + Nonprofit.findOne( + { name: new RegExp(req.params.nonprofitName.replace(/-/, ' '), 'i') }, + function(err, nonprofit) { + if (err) { return next(err); } + nonprofit.interestedCampers.push({ + username: req.user.profile.username, + picture: req.user.profile.picture, + timeOfInterest: Date.now() + }); + nonprofit.save(function(err) { + if (err) { return next(err); } + req.flash('success', { + msg: 'Thanks for expressing interest in this nonprofit project! ' + + "We've added you to this project as an interested camper!" + }); + res.redirect('back'); + }); + } + ); + } + } + */ +}; diff --git a/server/boot/passport.js b/server/boot/passport.js index 6c3c01bef4..326a575af0 100644 --- a/server/boot/passport.js +++ b/server/boot/passport.js @@ -1,68 +1,71 @@ -var express = require('express'), - passport = require('passport'), +/* +var passport = require('passport'), passportConf = require('../../config/passport'); -var router = express.Router(); -var passportOptions = { - successRedirect: '/', - failureRedirect: '/login' -}; - -router.all('/account', passportConf.isAuthenticated); - -router.get('/auth/twitter', passport.authenticate('twitter')); - -router.get( - '/auth/twitter/callback', - passport.authenticate('twitter', { +module.exports = function(app) { + var router = app.loopback.Router(); + var passportOptions = { successRedirect: '/', failureRedirect: '/login' - }) -); + }; -router.get( - '/auth/linkedin', - passport.authenticate('linkedin', { - state: 'SOME STATE' - }) -); + router.all('/account', passportConf.isAuthenticated); -router.get( - '/auth/linkedin/callback', - passport.authenticate('linkedin', passportOptions) -); + router.get('/auth/twitter', passport.authenticate('twitter')); -router.get( - '/auth/facebook', - passport.authenticate('facebook', {scope: ['email', 'user_location']}) -); + router.get( + '/auth/twitter/callback', + passport.authenticate('twitter', { + successRedirect: '/', + failureRedirect: '/login' + }) + ); -router.get( - '/auth/facebook/callback', - passport.authenticate('facebook', passportOptions), function (req, res) { - res.redirect(req.session.returnTo || '/'); - } -); + router.get( + '/auth/linkedin', + passport.authenticate('linkedin', { + state: 'SOME STATE' + }) + ); -router.get('/auth/github', passport.authenticate('github')); + router.get( + '/auth/linkedin/callback', + passport.authenticate('linkedin', passportOptions) + ); -router.get( - '/auth/github/callback', - passport.authenticate('github', passportOptions), function (req, res) { - res.redirect(req.session.returnTo || '/'); - } -); + router.get( + '/auth/facebook', + passport.authenticate('facebook', {scope: ['email', 'user_location']}) + ); -router.get( - '/auth/google', - passport.authenticate('google', {scope: 'profile email'}) -); + router.get( + '/auth/facebook/callback', + passport.authenticate('facebook', passportOptions), function (req, res) { + res.redirect(req.session.returnTo || '/'); + } + ); -router.get( - '/auth/google/callback', - passport.authenticate('google', passportOptions), function (req, res) { - res.redirect(req.session.returnTo || '/'); - } -); + router.get('/auth/github', passport.authenticate('github')); -module.exports = router; + router.get( + '/auth/github/callback', + passport.authenticate('github', passportOptions), function (req, res) { + res.redirect(req.session.returnTo || '/'); + } + ); + + router.get( + '/auth/google', + passport.authenticate('google', {scope: 'profile email'}) + ); + + router.get( + '/auth/google/callback', + passport.authenticate('google', passportOptions), function (req, res) { + res.redirect(req.session.returnTo || '/'); + } + ); + + app.use(router); +}; +*/ diff --git a/server/boot/randomAPIs.js b/server/boot/randomAPIs.js new file mode 100644 index 0000000000..d07ca1879a --- /dev/null +++ b/server/boot/randomAPIs.js @@ -0,0 +1,494 @@ +var Rx = require('rx'), + Twit = require('twit'), + async = require('async'), + moment = require('moment'), + Slack = require('node-slack'), + request = require('request'), + debug = require('debug')('freecc:cntr:resources'), + + constantStrings = require('../utils/constantStrings.json'), + secrets = require('../../config/secrets'); + +var slack = new Slack(secrets.slackHook); +module.exports = function(app) { + var router = app.loopback.Router(); + var User = app.models.User; + var Challenge = app.models.Challenge; + var Story = app.models.Store; + var FieldGuide = app.models.FieldGuide; + var Nonprofit = app.models.Nonprofit; + + 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); + + app.use(router); + + 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://freecodecamp.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.' + ].join(''), + 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'); + + // TODO(berks): refactor async to rx + async.parallel({ + users: function(callback) { + User.find( + { + where: { 'profile.username': { nlike: '' } }, + fields: { 'profile.username': true } + }, + function(err, users) { + if (err) { + debug('User err: ', err); + callback(err); + } else { + Rx.Observable.from(users) + .map(function(user) { + return user.profile.username; + }) + .toArray() + .subscribe( + function(usernames) { + callback(null, usernames); + }, + callback + ); + } + }); + }, + + challenges: function (callback) { + Challenge.find( + { fields: { name: true } }, + function (err, challenges) { + if (err) { + debug('Challenge err: ', err); + callback(err); + } else { + Rx.Observable.from(challenges) + .map(function(challenge) { + return challenge.name; + }) + .toArray() + .subscribe( + callback.bind(callback, null), + callback + ); + } + }); + }, + stories: function (callback) { + Story.find( + { field: { link: true } }, + function (err, stories) { + if (err) { + debug('Story err: ', err); + callback(err); + } else { + Rx.Observable.from(stories) + .map(function(story) { + return story.link; + }) + .toArray() + .subscribe( + callback.bind(callback, null), + callback + ); + } + } + ); + }, + nonprofits: function (callback) { + Nonprofit.find( + { field: { name: true } }, + function(err, nonprofits) { + if (err) { + debug('User err: ', err); + callback(err); + } else { + Rx.Observable.from(nonprofits) + .map(function(nonprofit) { + return nonprofit.name; + }) + .toArray() + .subscribe( + callback.bind(callback, null), + callback + ); + } + }); + }, + fieldGuides: function(callback) { + FieldGuide.find( + { field: { name: true } }, + function(err, fieldGuides) { + if (err) { + debug('User err: ', err); + callback(err); + } else { + Rx.Observable.from(fieldGuides) + .map(function(fieldGuide) { + return fieldGuide.name; + }) + .toArray() + .subscribe( + callback.bind(callback, null), + callback + ); + } + }); + } + }, function(err, results) { + if (err) { + return next(err); + } + 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://freecodecamp.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) { + 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) { + 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) { + 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' + } + ] + ); + } +}; diff --git a/server/boot/redirects.js b/server/boot/redirects.js index c5422b6986..bac16345af 100644 --- a/server/boot/redirects.js +++ b/server/boot/redirects.js @@ -1,47 +1,50 @@ -var express = require('express'); -var router = express.Router(); +module.exports = function(app) { + var router = app.loopback.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('/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('/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('/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('/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('/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('/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('/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('/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('/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?'); -}); + router.get('/privacy', function(req, res) { + res.redirect( + 301, '/field-guide/what-is-the-free-code-camp-privacy-policy?' + ); + }); -module.exports = router; + app.use(router); +}; diff --git a/server/boot/restApi.js b/server/boot/restApi.js new file mode 100644 index 0000000000..e81c692006 --- /dev/null +++ b/server/boot/restApi.js @@ -0,0 +1,4 @@ +module.exports = function mountRestApi(app) { + var restApiRoot = app.get('restApiRoot'); + app.use(restApiRoot, app.loopback.rest()); +}; diff --git a/server/boot/story.js b/server/boot/story.js index ef5fc134ac..b28580aa09 100755 --- a/server/boot/story.js +++ b/server/boot/story.js @@ -1,394 +1,456 @@ var nodemailer = require('nodemailer'), sanitizeHtml = require('sanitize-html'), - express = require('express'), moment = require('moment'), mongodb = require('mongodb'), // debug = require('debug')('freecc:cntr:story'), - Story = require('../../common/models/Story'), - Comment = require('../../common/models/Comment'), - User = require('../../common/models/User'), - resources = require('../resources/resources'), + utils = require('../utils'), MongoClient = mongodb.MongoClient, - secrets = require('../../config/secrets'), - router = express.Router(); + secrets = require('../../config/secrets'); -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); +module.exports = function(app) { + var router = app.loopback.Router(); + var User = app.models.User; + var Story = app.models.Story; -function hotRank(timeValue, rank) { - /* - * Hotness ranking algorithm: http://amix.dk/blog/post/19588 - * tMS = postedOnDate - foundationTime; - * Ranking... - * f(ts, 1, rank) = log(10)z + (ts)/45000; - */ - var time48Hours = 172800000; - var hotness; - var z = Math.log(rank) / Math.log(10); - hotness = z + (timeValue / time48Hours); - return hotness; + 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); -} + app.use(router); -function hotJSON(req, res, next) { - var story = Story.find({}).sort({'timePosted': -1}).limit(1000); - story.exec(function(err, stories) { - if (err) { - return next(err); - } - - var foundationDate = 1413298800000; - - var sliceVal = stories.length >= 100 ? 100 : stories.length; - return res.json(stories.map(function(elem) { - return elem; - }).sort(function(a, b) { - return hotRank(b.timePosted - foundationDate, b.rank, b.headline) - - hotRank(a.timePosted - foundationDate, a.rank, a.headline); - }).slice(0, sliceVal)); - - }); -} - -function recentJSON(req, res, next) { - var story = Story.find({}).sort({'timePosted': -1}).limit(100); - story.exec(function(err, stories) { - if (err) { - return next(err); - } - return res.json(stories); - }); -} - -function hot(req, res) { - return res.render('stories/index', { - title: 'Hot stories currently trending on Camper News', - page: 'hot' - }); -} - -function submitNew(req, res) { - return res.render('stories/index', { - title: 'Submit a new story to Camper News', - page: 'submit' - }); -} - -/* - * no used anywhere -function search(req, res) { - return res.render('stories/index', { - title: 'Search the archives of Camper News', - page: 'search' - }); -} - -function recent(req, res) { - return res.render('stories/index', { - title: 'Recently submitted stories on Camper News', - page: 'recent' - }); -} -*/ - -function preSubmit(req, res) { - - var data = req.query; - var cleanData = sanitizeHtml(data.url, { - allowedTags: [], - allowedAttributes: [] - }).replace(/";/g, '"'); - if (data.url.replace(/&/g, '&') !== cleanData) { - - req.flash('errors', { - msg: 'The data for this post is malformed' - }); - return res.render('stories/index', { - page: 'stories/submit' - }); + function hotRank(timeValue, rank) { + /* + * Hotness ranking algorithm: http://amix.dk/blog/post/19588 + * tMS = postedOnDate - foundationTime; + * Ranking... + * f(ts, 1, rank) = log(10)z + (ts)/45000; + */ + var time48Hours = 172800000; + var hotness; + var z = Math.log(rank) / Math.log(10); + hotness = z + (timeValue / time48Hours); + return hotness; } - var title = data.title || ''; - var image = data.image || ''; - var description = data.description || ''; - return res.render('stories/index', { - title: 'Confirm your Camper News story submission', - page: 'storySubmission', - storyURL: data.url, - storyTitle: title, - storyImage: image, - storyMetaDescription: description - }); -} - - -function returnIndividualStory(req, res, next) { - var dashedName = req.params.storyName; - - var storyName = dashedName.replace(/\-/g, ' ').trim(); - - Story.find({'storyLink': storyName}, function(err, story) { - if (err) { - return next(err); - } - - - if (story.length < 1) { - req.flash('errors', { - msg: "404: We couldn't find a story with that name. " + - 'Please double check the name.' - }); - - return res.redirect('/news/'); - } - - story = story.pop(); - var dashedNameFull = story.storyLink.toLowerCase() - .replace(/\s+/g, ' ') - .replace(/\s/g, '-'); - if (dashedNameFull !== dashedName) { - return res.redirect('../news/' + dashedNameFull); - } - - var userVoted = false; - try { - var votedObj = story.upVotes.filter(function(a) { - return a['upVotedByUsername'] === req.user['profile']['username']; - }); - if (votedObj.length > 0) { - userVoted = true; - } - } catch(e) { - userVoted = false; - } - res.render('stories/index', { - title: story.headline, - link: story.link, - originalStoryLink: dashedName, - originalStoryAuthorEmail: story.author.email || '', - author: story.author, - description: story.description, - rank: story.upVotes.length, - upVotes: story.upVotes, - comments: story.comments, - id: story._id, - timeAgo: moment(story.timePosted).fromNow(), - image: story.image, - page: 'show', - storyMetaDescription: story.metaDescription, - hasUserVoted: userVoted - }); - }); -} - -function getStories(req, res, next) { - MongoClient.connect(secrets.db, function(err, database) { - if (err) { - return next(err); - } - database.collection('stories').find({ - '$text': { - '$search': req.body.data.searchValue - } - }, { - headline: 1, - timePosted: 1, - link: 1, - description: 1, - rank: 1, - upVotes: 1, - author: 1, - comments: 1, - image: 1, - storyLink: 1, - metaDescription: 1, - textScore: { - $meta: 'textScore' - } - }, { - sort: { - textScore: { - $meta: 'textScore' - } - } - }).toArray(function(err, items) { + function hotJSON(req, res, next) { + var story = Story.find({}).sort({'timePosted': -1}).limit(1000); + story.exec(function(err, stories) { if (err) { return next(err); } - if (items !== null && items.length !== 0) { - return res.json(items); - } - return res.sendStatus(404); + + var foundationDate = 1413298800000; + + var sliceVal = stories.length >= 100 ? 100 : stories.length; + return res.json(stories.map(function(elem) { + return elem; + }).sort(function(a, b) { + return hotRank(b.timePosted - foundationDate, b.rank, b.headline) + - hotRank(a.timePosted - foundationDate, a.rank, a.headline); + }).slice(0, sliceVal)); + }); - }); -} + } -function upvote(req, res, next) { - var data = req.body.data; - Story.find({'_id': data.id}, function(err, story) { - if (err) { - return next(err); - } - story = story.pop(); - story.rank++; - story.upVotes.push( - { - upVotedBy: req.user._id, - upVotedByUsername: req.user.profile.username + function recentJSON(req, res, next) { + var story = Story.find({}).sort({'timePosted': -1}).limit(100); + story.exec(function(err, stories) { + if (err) { + return next(err); } - ); - story.markModified('rank'); - story.save(); - // NOTE(Berks): This logic is full of wholes and race conditions - // this could be the source of many 'can't set headers after they are sent' - // errors. This needs cleaning - User.findOne({'_id': story.author.userId}, function(err, user) { - if (err) { return next(err); } + return res.json(stories); + }); + } - user.progressTimestamps.push(Date.now() || 0); - user.save(function (err) { - req.user.save(function (err) { - if (err) { return next(err); } + function hot(req, res) { + return res.render('stories/index', { + title: 'Hot stories currently trending on Camper News', + page: 'hot' + }); + } + + function submitNew(req, res) { + return res.render('stories/index', { + title: 'Submit a new story to Camper News', + page: 'submit' + }); + } + + /* + * no used anywhere + function search(req, res) { + return res.render('stories/index', { + title: 'Search the archives of Camper News', + page: 'search' + }); + } + + function recent(req, res) { + return res.render('stories/index', { + title: 'Recently submitted stories on Camper News', + page: 'recent' + }); + } + */ + + function preSubmit(req, res) { + + var data = req.query; + var cleanData = sanitizeHtml(data.url, { + allowedTags: [], + allowedAttributes: [] + }).replace(/";/g, '"'); + if (data.url.replace(/&/g, '&') !== cleanData) { + + req.flash('errors', { + msg: 'The data for this post is malformed' + }); + return res.render('stories/index', { + page: 'stories/submit' + }); + } + + var title = data.title || ''; + var image = data.image || ''; + var description = data.description || ''; + return res.render('stories/index', { + title: 'Confirm your Camper News story submission', + page: 'storySubmission', + storyURL: data.url, + storyTitle: title, + storyImage: image, + storyMetaDescription: description + }); + } + + + function returnIndividualStory(req, res, next) { + var dashedName = req.params.storyName; + + var storyName = dashedName.replace(/\-/g, ' ').trim(); + + Story.find({'storyLink': storyName}, function(err, story) { + if (err) { + return next(err); + } + + + if (story.length < 1) { + req.flash('errors', { + msg: "404: We couldn't find a story with that name. " + + 'Please double check the name.' }); - req.user.progressTimestamps.push(Date.now() || 0); + + return res.redirect('/news/'); + } + + story = story.pop(); + var dashedNameFull = story.storyLink.toLowerCase() + .replace(/\s+/g, ' ') + .replace(/\s/g, '-'); + if (dashedNameFull !== dashedName) { + return res.redirect('../news/' + dashedNameFull); + } + + var userVoted = false; + try { + var votedObj = story.upVotes.filter(function(a) { + return a['upVotedByUsername'] === req.user['profile']['username']; + }); + if (votedObj.length > 0) { + userVoted = true; + } + } catch(e) { + userVoted = false; + } + res.render('stories/index', { + title: story.headline, + link: story.link, + originalStoryLink: dashedName, + originalStoryAuthorEmail: story.author.email || '', + author: story.author, + description: story.description, + rank: story.upVotes.length, + upVotes: story.upVotes, + comments: story.comments, + id: story._id, + timeAgo: moment(story.timePosted).fromNow(), + image: story.image, + page: 'show', + storyMetaDescription: story.metaDescription, + hasUserVoted: userVoted + }); + }); + } + + function getStories(req, res, next) { + MongoClient.connect(secrets.db, function(err, database) { + if (err) { + return next(err); + } + database.collection('stories').find({ + '$text': { + '$search': req.body.data.searchValue + } + }, { + headline: 1, + timePosted: 1, + link: 1, + description: 1, + rank: 1, + upVotes: 1, + author: 1, + comments: 1, + image: 1, + storyLink: 1, + metaDescription: 1, + textScore: { + $meta: 'textScore' + } + }, { + sort: { + textScore: { + $meta: 'textScore' + } + } + }).toArray(function(err, items) { if (err) { return next(err); } + if (items !== null && items.length !== 0) { + return res.json(items); + } + return res.sendStatus(404); }); }); - return res.send(story); - }); -} - -function comments(req, res, next) { - var data = req.params.id; - Comment.find({'_id': data}, function(err, comment) { - if (err) { - return next(err); - } - comment = comment.pop(); - return res.send(comment); - }); -} - -function newStory(req, res, next) { - if (!req.user) { - return next(new Error('Must be logged in')); } - var url = req.body.data.url; - var cleanURL = sanitizeHtml(url, { - allowedTags: [], - allowedAttributes: [] - }).replace(/"/g, '"'); - if (cleanURL !== url) { - req.flash('errors', { - msg: "The URL you submitted doesn't appear valid" - }); - return res.json({ - alreadyPosted: true, - storyURL: '/stories/submit' + + function upvote(req, res, next) { + var data = req.body.data; + Story.find({'_id': data.id}, function(err, story) { + if (err) { + return next(err); + } + story = story.pop(); + story.rank++; + story.upVotes.push( + { + upVotedBy: req.user._id, + upVotedByUsername: req.user.profile.username + } + ); + story.markModified('rank'); + story.save(); + // NOTE(Berks): This logic is full of wholes and race conditions + // this could be the source of many 'can't set headers after + // they are sent' + // errors. This needs cleaning + User.findOne( + { where: { id: story.author.userId } }, + function(err, user) { + if (err) { return next(err); } + + user.progressTimestamps.push(Date.now() || 0); + user.save(function (err) { + req.user.save(function (err) { + if (err) { return next(err); } + }); + req.user.progressTimestamps.push(Date.now() || 0); + if (err) { + return next(err); + } + }); + } + ); + return res.send(story); }); + } + function comments(req, res, next) { + var data = req.params.id; + Comment.find( + { where: {'_id': data } }, + function(err, comment) { + if (err) { + return next(err); + } + comment = comment.pop(); + return res.send(comment); + }); } - if (url.search(/^https?:\/\//g) === -1) { - url = 'http://' + url; - } - Story.find({'link': url}, function(err, story) { - if (err) { - return next(err); + + function newStory(req, res, next) { + if (!req.user) { + return next(new Error('Must be logged in')); } - if (story.length) { + var url = req.body.data.url; + var cleanURL = sanitizeHtml(url, { + allowedTags: [], + allowedAttributes: [] + }).replace(/"/g, '"'); + if (cleanURL !== url) { req.flash('errors', { - msg: "Someone's already posted that link. Here's the discussion." + msg: "The URL you submitted doesn't appear valid" }); return res.json({ alreadyPosted: true, - storyURL: '/news/' + story.pop().storyLink + storyURL: '/stories/submit' }); - } - resources.getURLTitle(url, processResponse); - }); - function processResponse(err, story) { - if (err) { - res.json({ - alreadyPosted: false, - storyURL: url, - storyTitle: '', - storyImage: '', - storyMetaDescription: '' - }); - } else { - res.json({ - alreadyPosted: false, - storyURL: url, - storyTitle: story.title, - storyImage: story.image, - storyMetaDescription: story.description - }); + } + if (url.search(/^https?:\/\//g) === -1) { + url = 'http://' + url; + } + Story.find( + { where: {'link': url} }, + function(err, story) { + if (err) { + return next(err); + } + if (story.length) { + req.flash('errors', { + msg: "Someone's already posted that link. Here's the discussion." + }); + return res.json({ + alreadyPosted: true, + storyURL: '/news/' + story.pop().storyLink + }); + } + utils.getURLTitle(url, processResponse); + } + ); + + function processResponse(err, story) { + if (err) { + res.json({ + alreadyPosted: false, + storyURL: url, + storyTitle: '', + storyImage: '', + storyMetaDescription: '' + }); + } else { + res.json({ + alreadyPosted: false, + storyURL: url, + storyTitle: story.title, + storyImage: story.image, + storyMetaDescription: story.description + }); + } } } -} -function storySubmission(req, res, next) { - var data = req.body.data; - if (!req.user) { - return next(new Error('Not authorized')); - } - var storyLink = data.headline - .replace(/[^a-z0-9\s]/gi, '') - .replace(/\s+/g, ' ') - .toLowerCase() - .trim(); - - var link = data.link; - - if (link.search(/^https?:\/\//g) === -1) { - link = 'http://' + link; - } - - Story.count({ - storyLink: new RegExp('^' + storyLink + '(?: [0-9]+)?$', 'i') - }, function (err, storyCount) { - if (err) { - return next(err); + function storySubmission(req, res, next) { + var data = req.body.data; + if (!req.user) { + return next(new Error('Not authorized')); } - - // if duplicate storyLink add unique number - storyLink = (storyCount === 0) ? storyLink : storyLink + ' ' + storyCount; + var storyLink = data.headline + .replace(/[^a-z0-9\s]/gi, '') + .replace(/\s+/g, ' ') + .toLowerCase() + .trim(); var link = data.link; + if (link.search(/^https?:\/\//g) === -1) { link = 'http://' + link; } - var story = new Story({ - headline: sanitizeHtml(data.headline, { + + Story.count({ + storyLink: { like: new RegExp('^' + storyLink + '(?: [0-9]+)?$', 'i') } + }, function (err, storyCount) { + if (err) { + return next(err); + } + + // if duplicate storyLink add unique number + storyLink = (storyCount === 0) ? storyLink : storyLink + ' ' + storyCount; + + var link = data.link; + if (link.search(/^https?:\/\//g) === -1) { + link = 'http://' + link; + } + var story = new Story({ + headline: sanitizeHtml(data.headline, { + allowedTags: [], + allowedAttributes: [] + }).replace(/"/g, '"'), + timePosted: Date.now(), + link: link, + description: sanitizeHtml(data.description, { + allowedTags: [], + allowedAttributes: [] + }).replace(/"/g, '"'), + rank: 1, + upVotes: [({ + upVotedBy: req.user._id, + upVotedByUsername: req.user.profile.username + })], + author: { + picture: req.user.profile.picture, + userId: req.user._id, + username: req.user.profile.username, + email: req.user.email + }, + comments: [], + image: data.image, + storyLink: storyLink, + metaDescription: data.storyMetaDescription, + originalStoryAuthorEmail: req.user.email + }); + story.save(function (err) { + if (err) { + return next(err); + } + req.user.progressTimestamps.push(Date.now() || 0); + req.user.save(function (err) { + if (err) { + return next(err); + } + res.send(JSON.stringify({ + storyLink: story.storyLink.replace(/\s+/g, '-').toLowerCase() + })); + }); + }); + }); + } + + 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, '"'), - timePosted: Date.now(), - link: link, - description: sanitizeHtml(data.description, { - allowedTags: [], - allowedAttributes: [] - }).replace(/"/g, '"'), - rank: 1, - upVotes: [({ - upVotedBy: req.user._id, - upVotedByUsername: req.user.profile.username - })], + }).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, @@ -396,214 +458,164 @@ function storySubmission(req, res, next) { email: req.user.email }, comments: [], - image: data.image, - storyLink: storyLink, - metaDescription: data.storyMetaDescription, - originalStoryAuthorEmail: req.user.email + topLevel: true, + commentOn: Date.now() }); - story.save(function (err) { - if (err) { - return next(err); - } - req.user.progressTimestamps.push(Date.now() || 0); - req.user.save(function (err) { - if (err) { - return next(err); - } - res.send(JSON.stringify({ - storyLink: story.storyLink.replace(/\s+/g, '-').toLowerCase() - })); - }); - }); - }); -} -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')); + commentSave(comment, Story, res, next); } - 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) { + 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, '"'); - var sanitizedBody = sanitizeHtml(req.body.body, { - allowedTags: [], - allowedAttributes: [] - }).replace(/"/g, '"'); - if (req.body.body !== sanitizedBody) { + if (data.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) { + 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); } - res.send(true); - }); + cmt = cmt.pop(); - }); + 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); + } -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) { + cmt.body = sanitizedBody; + cmt.commentOn = Date.now(); + cmt.save(function(err) { 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) { + 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); } - // 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) { + associatedContext = associatedContext.pop(); + if (associatedContext) { + associatedContext.comments.push(data._id); + associatedContext.save(function (err) { if (err) { - return err; + return next(err); } + res.send(true); }); } - }); - }); - } catch (e) { - return next(err); - } - }); -} + // 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 + } + }); -module.exports = router; + 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) { + return next(err); + } + }); + } +}; diff --git a/server/boot/user.js b/server/boot/user.js index 8d3f7f1cfb..d008c3649d 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -3,804 +3,879 @@ var _ = require('lodash'), async = require('async'), crypto = require('crypto'), nodemailer = require('nodemailer'), - passport = require('passport'), moment = require('moment'), - express = require('express'), debug = require('debug')('freecc:cntr:userController'), - User = require('../../common/models/User'), - secrets = require('../../config/secrets'), - resources = require('./../resources/resources'); + secrets = require('../../config/secrets'); -var router = express.Router(); -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); -// Ensure this is the last route! -router.get('/:username', returnUser); +module.exports = function(app) { + var router = app.loopback.Router(); + var User = app.models.User; + var Story = app.models.Story; + var Comment = app.models.Comment; -/** - * GET /signin - * Siginin page. - */ - -function getSignin (req, res) { - if (req.user) { - return res.redirect('/'); - } - res.render('account/signin', { - title: 'Free Code Camp Login' + 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); + // Ensure this is the last route! + router.get('/:username', returnUser); -/** - * POST /signin - * Sign in using email and password. - */ + app.use(router); -function postSignin (req, res, next) { - req.assert('email', 'Email is not valid').isEmail(); - req.assert('password', 'Password cannot be blank').notEmpty(); + /** + * GET /signin + * Siginin page. + */ - var errors = req.validationErrors(); - - if (errors) { - req.flash('errors', errors); - return res.redirect('/signin'); + function getSignin (req, res) { + if (req.user) { + return res.redirect('/'); + } + res.render('account/signin', { + title: 'Free Code Camp Login' + }); } - passport.authenticate('local', function(err, user, info) { - if (err) { - return next(err); - } - if (!user) { - req.flash('errors', { msg: info.message }); + /** + * POST /signin + * Sign in using email and password. + */ + + /* + * TODO(berks): this should be done using loopback + function postSignin (req, res, next) { + req.assert('email', 'Email is not valid').isEmail(); + req.assert('password', 'Password cannot be blank').notEmpty(); + + var errors = req.validationErrors(); + + if (errors) { + req.flash('errors', errors); return res.redirect('/signin'); } - req.logIn(user, function(err) { + + passport.authenticate('local', function(err, user, info) { if (err) { return next(err); } - req.flash('success', { msg: 'Success! You are logged in.' }); - if (/hotStories/.test(req.session.returnTo)) { - return res.redirect('../news'); + if (!user) { + req.flash('errors', { msg: info.message }); + return res.redirect('/signin'); } - if (/field-guide/.test(req.session.returnTo)) { - return res.redirect('../field-guide'); - } - return res.redirect(req.session.returnTo || '/'); - }); - })(req, res, next); -} - -/** - * GET /signout - * Log out. - */ - -function signout (req, res) { - req.logout(); - res.redirect('/'); -} - -/** - * GET /email-signup - * Signup page. - */ - -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. - */ - -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. - */ - -function postEmailSignup (req, res, next) { - req.assert('email', 'valid email required').isEmail(); - var errors = req.validationErrors(); - - if (errors) { - req.flash('errors', errors); - return res.redirect('/email-signup'); - } - - var possibleUserData = req.body; - - if (possibleUserData.password.length < 8) { - req.flash('errors', { - msg: 'Your password is too short' - }); - return res.redirect('email-signup'); - } - - if (possibleUserData.username.length < 5 || possibleUserData.length > 20) { - req.flash('errors', { - msg: 'Your username must be between 5 and 20 characters' - }); - return res.redirect('email-signup'); - } - - - var user = new User({ - email: req.body.email.trim(), - password: req.body.password, - profile: { - username: req.body.username.trim(), - picture: - 'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png' - } - }); - - User.findOne({ email: req.body.email }, function(err, existingEmail) { - if (err) { - return next(err); - } - - if (existingEmail) { - req.flash('errors', { - msg: 'Account with that email address already exists.' + req.logIn(user, function(err) { + if (err) { + return next(err); + } + req.flash('success', { msg: 'Success! You are logged in.' }); + if (/hotStories/.test(req.session.returnTo)) { + return res.redirect('../news'); + } + if (/field-guide/.test(req.session.returnTo)) { + return res.redirect('../field-guide'); + } + return res.redirect(req.session.returnTo || '/'); }); - return res.redirect('/email-signup'); + })(req, res, next); + } + */ + + /** + * GET /signout + * Log out. + */ + + function signout (req, res) { + req.logout(); + res.redirect('/'); + } + + /** + * GET /email-signup + * Signup page. + */ + + function getEmailSignin (req, res) { + if (req.user) { + return res.redirect('/'); } - User.findOne( - { 'profile.username': req.body.username }, - function(err, existingUsername) { + res.render('account/email-signin', { + title: 'Sign in to your Free Code Camp Account' + }); + } + + /** + * GET /signin + * Signup page. + */ + + 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. + */ + + function postEmailSignup (req, res, next) { + req.assert('email', 'valid email required').isEmail(); + var errors = req.validationErrors(); + + if (errors) { + req.flash('errors', errors); + return res.redirect('/email-signup'); + } + + var possibleUserData = req.body; + + if (possibleUserData.password.length < 8) { + req.flash('errors', { + msg: 'Your password is too short' + }); + return res.redirect('email-signup'); + } + + if (possibleUserData.username.length < 5 || possibleUserData.length > 20) { + req.flash('errors', { + msg: 'Your username must be between 5 and 20 characters' + }); + return res.redirect('email-signup'); + } + + + var user = new User({ + email: req.body.email.trim(), + password: req.body.password, + profile: { + username: req.body.username.trim(), + picture: + 'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png' + } + }); + + User.findOne({ email: req.body.email }, function(err, existingEmail) { if (err) { return next(err); } - if (existingUsername) { + + if (existingEmail) { req.flash('errors', { - msg: 'Account with that username already exists.' + msg: 'Account with that email address already exists.' }); return res.redirect('/email-signup'); } + User.findOne( + { 'profile.username': req.body.username }, + function(err, existingUsername) { + if (err) { + return next(err); + } + if (existingUsername) { + req.flash('errors', { + msg: 'Account with that username already exists.' + }); + return res.redirect('/email-signup'); + } - user.save(function(err) { - if (err) { return next(err); } - req.logIn(user, function(err) { + user.save(function(err) { if (err) { return next(err); } - res.redirect('/email-signup'); + req.logIn(user, function(err) { + if (err) { return next(err); } + res.redirect('/email-signup'); + }); + }); + var transporter = nodemailer.createTransport({ + service: 'Mandrill', + auth: { + user: secrets.mandrill.user, + pass: secrets.mandrill.password + } + }); + var mailOptions = { + to: user.email, + from: 'Team@freecodecamp.com', + subject: 'Welcome to Free Code Camp!', + text: [ + 'Greetings from San Francisco!\n\n', + 'Thank you for joining our community.\n', + 'Feel free to email us at this address if you have ', + 'any questions about Free Code Camp.\n', + 'And if you have a moment, check out our blog: ', + 'blog.freecodecamp.com.\n', + 'Good luck with the challenges!\n\n', + '- the Free Code Camp Volunteer Team' + ].join('') + }; + transporter.sendMail(mailOptions, function(err) { + if (err) { return err; } }); }); - var transporter = nodemailer.createTransport({ - service: 'Mandrill', - auth: { - user: secrets.mandrill.user, - pass: secrets.mandrill.password - } - }); - var mailOptions = { - to: user.email, - from: 'Team@freecodecamp.com', - subject: 'Welcome to Free Code Camp!', - text: [ - 'Greetings from San Francisco!\n\n', - 'Thank you for joining our community.\n', - 'Feel free to email us at this address if you have ', - 'any questions about Free Code Camp.\n', - 'And if you have a moment, check out our blog: ', - 'blog.freecodecamp.com.\n', - 'Good luck with the challenges!\n\n', - '- the Free Code Camp Volunteer Team' - ].join('') - }; - transporter.sendMail(mailOptions, function(err) { - if (err) { return err; } - }); }); - }); -} + } -/** - * GET /account - * Profile page. - */ + /** + * GET /account + * Profile page. + */ -function getAccount (req, res) { - res.render('account/account', { - title: 'Manage your Free Code Camp Account' - }); -} + function getAccount (req, res) { + res.render('account/account', { + title: 'Manage your Free Code Camp Account' + }); + } -/** - * Angular API Call - */ + /** + * Angular API Call + */ -function getAccountAngular (req, res) { - res.json({ - user: req.user - }); -} + function getAccountAngular (req, res) { + res.json({ + user: req.user + }); + } -/** - * Unique username check API Call - */ + /** + * Unique username check API Call + */ -function checkUniqueUsername (req, res, next) { - User.count( - { 'profile.username': req.params.username.toLowerCase() }, - function (err, data) { - if (err) { return next(err); } - if (data === 1) { - return res.send(true); - } else { - return res.send(false); - } - }); -} - -/** - * Existing username check - */ - -function checkExistingUsername (req, res, next) { - User.count( - { 'profile.username': req.params.username.toLowerCase() }, - function (err, data) { - if (err) { return next(err); } - if (data === 1) { - return res.send(true); - } else { - return res.send(false); - } - } - ); -} - -/** - * Unique email check API Call - */ - -function checkUniqueEmail (req, res, next) { - User.count( - { email: decodeURIComponent(req.params.email).toLowerCase() }, - function (err, data) { + function checkUniqueUsername (req, res, next) { + User.count( + { 'profile.username': req.params.username.toLowerCase() }, + function (err, data) { if (err) { return next(err); } if (data === 1) { return res.send(true); } else { return res.send(false); } - } - ); -} + }); + } + /** + * Existing username check + */ -/** - * GET /campers/:username - * Public Profile page. - */ - -function returnUser (req, res, next) { - User.find( - { 'profile.username': req.params.username.toLowerCase() }, - function(err, user) { - if (err) { - debug('Username err: ', err); - return next(err); - } - if (user[0]) { - user = user[0]; - - user.progressTimestamps = user.progressTimestamps.sort(function(a, b) { - return a - b; - }); - - var timeObject = Object.create(null); - R.forEach(function(time) { - timeObject[moment(time).format('YYYY-MM-DD')] = time; - }, user.progressTimestamps); - - var tmpLongest = 1; - var timeKeys = R.keys(timeObject); - - user.longestStreak = 0; - for (var i = 1; i <= timeKeys.length; i++) { - if (moment(timeKeys[i - 1]).add(1, 'd').toString() - === moment(timeKeys[i]).toString()) { - tmpLongest++; - if (tmpLongest > user.longestStreak) { - user.longestStreak = tmpLongest; - } - } else { - tmpLongest = 1; - } + function checkExistingUsername (req, res, next) { + User.count( + { 'profile.username': req.params.username.toLowerCase() }, + function (err, data) { + if (err) { return next(err); } + if (data === 1) { + return res.send(true); + } else { + return res.send(false); } + } + ); + } - timeKeys = timeKeys.reverse(); - tmpLongest = 1; + /** + * Unique email check API Call + */ - user.currentStreak = 1; - var today = moment(Date.now()).format('YYYY-MM-DD'); + function checkUniqueEmail (req, res, next) { + User.count( + { email: decodeURIComponent(req.params.email).toLowerCase() }, + function (err, data) { + if (err) { return next(err); } + if (data === 1) { + return res.send(true); + } else { + return res.send(false); + } + } + ); + } - if ( - moment(today).toString() === moment(timeKeys[0]).toString() || - moment(today).subtract(1, 'd').toString() === - moment(timeKeys[0]).toString() - ) { - for (var _i = 1; _i <= timeKeys.length; _i++) { - if ( - moment(timeKeys[_i - 1]).subtract(1, 'd').toString() === - moment(timeKeys[_i]).toString() - ) { + /** + * GET /campers/:username + * Public Profile page. + */ + function returnUser (req, res, next) { + User.find( + { 'profile.username': req.params.username.toLowerCase() }, + function(err, user) { + if (err) { + debug('Username err: ', err); + return next(err); + } + if (user[0]) { + user = user[0]; + user.progressTimestamps = + user.progressTimestamps.sort(function(a, b) { + return a - b; + }); + + var timeObject = Object.create(null); + R.forEach(function(time) { + timeObject[moment(time).format('YYYY-MM-DD')] = time; + }, user.progressTimestamps); + + var tmpLongest = 1; + var timeKeys = R.keys(timeObject); + + user.longestStreak = 0; + for (var i = 1; i <= timeKeys.length; i++) { + if (moment(timeKeys[i - 1]).add(1, 'd').toString() + === moment(timeKeys[i]).toString()) { tmpLongest++; - - if (tmpLongest > user.currentStreak) { - user.currentStreak = tmpLongest; + if (tmpLongest > user.longestStreak) { + user.longestStreak = tmpLongest; } } else { - break; + tmpLongest = 1; } } - } else { - user.currentStreak = 1; - } - user.save(function(err) { - if (err) { - return next(err); + timeKeys = timeKeys.reverse(); + tmpLongest = 1; + + user.currentStreak = 1; + var today = moment(Date.now()).format('YYYY-MM-DD'); + + if ( + moment(today).toString() === moment(timeKeys[0]).toString() || + moment(today).subtract(1, 'd').toString() === + moment(timeKeys[0]).toString() + ) { + for (var _i = 1; _i <= timeKeys.length; _i++) { + + if ( + moment(timeKeys[_i - 1]).subtract(1, 'd').toString() === + moment(timeKeys[_i]).toString() + ) { + + tmpLongest++; + + if (tmpLongest > user.currentStreak) { + user.currentStreak = tmpLongest; + } + } else { + break; + } + } + } else { + user.currentStreak = 1; } - var data = {}; - var progressTimestamps = user.progressTimestamps; - progressTimestamps.forEach(function(timeStamp) { - data[(timeStamp / 1000)] = 1; - }); + user.save(function(err) { + if (err) { + return next(err); + } - user.currentStreak = user.currentStreak || 1; - user.longestStreak = user.longestStreak || 1; - var challenges = user.completedChallenges.filter(function ( obj ) { - return obj.challengeType === 3 || obj.challengeType === 4; - }); + var data = {}; + var progressTimestamps = user.progressTimestamps; + progressTimestamps.forEach(function(timeStamp) { + data[(timeStamp / 1000)] = 1; + }); - res.render('account/show', { - title: 'Camper ' + user.profile.username + '\'s portfolio', - username: user.profile.username, - name: user.profile.name, - location: user.profile.location, - githubProfile: user.profile.githubProfile, - linkedinProfile: user.profile.linkedinProfile, - codepenProfile: user.profile.codepenProfile, - facebookProfile: user.profile.facebookProfile, - twitterHandle: user.profile.twitterHandle, - bio: user.profile.bio, - picture: user.profile.picture, - progressTimestamps: user.progressTimestamps, - website1Link: user.portfolio.website1Link, - website1Title: user.portfolio.website1Title, - website1Image: user.portfolio.website1Image, - website2Link: user.portfolio.website2Link, - website2Title: user.portfolio.website2Title, - website2Image: user.portfolio.website2Image, - website3Link: user.portfolio.website3Link, - website3Title: user.portfolio.website3Title, - website3Image: user.portfolio.website3Image, - challenges: challenges, - bonfires: user.completedChallenges.filter(function(challenge) { - return challenge.challengeType === 5; - }), - calender: data, - moment: moment, - longestStreak: user.longestStreak + - (user.longestStreak === 1 ? ' day' : ' days'), - currentStreak: user.currentStreak + - (user.currentStreak === 1 ? ' day' : ' days') + user.currentStreak = user.currentStreak || 1; + user.longestStreak = user.longestStreak || 1; + var challenges = user.completedChallenges.filter(function ( obj ) { + return obj.challengeType === 3 || obj.challengeType === 4; + }); + + res.render('account/show', { + title: 'Camper ' + user.profile.username + '\'s portfolio', + username: user.profile.username, + name: user.profile.name, + location: user.profile.location, + githubProfile: user.profile.githubProfile, + linkedinProfile: user.profile.linkedinProfile, + codepenProfile: user.profile.codepenProfile, + facebookProfile: user.profile.facebookProfile, + twitterHandle: user.profile.twitterHandle, + bio: user.profile.bio, + picture: user.profile.picture, + progressTimestamps: user.progressTimestamps, + website1Link: user.portfolio.website1Link, + website1Title: user.portfolio.website1Title, + website1Image: user.portfolio.website1Image, + website2Link: user.portfolio.website2Link, + website2Title: user.portfolio.website2Title, + website2Image: user.portfolio.website2Image, + website3Link: user.portfolio.website3Link, + website3Title: user.portfolio.website3Title, + website3Image: user.portfolio.website3Image, + challenges: challenges, + bonfires: user.completedChallenges.filter(function(challenge) { + return challenge.challengeType === 5; + }), + calender: data, + moment: moment, + longestStreak: user.longestStreak + + (user.longestStreak === 1 ? ' day' : ' days'), + currentStreak: user.currentStreak + + (user.currentStreak === 1 ? ' day' : ' days') + }); }); - }); - } else { - req.flash('errors', { - msg: "404: We couldn't find a page with that url. " + - 'Please double check the link.' - }); - return res.redirect('/'); + } else { + req.flash('errors', { + msg: "404: We couldn't find a page with that url. " + + 'Please double check the link.' + }); + return res.redirect('/'); + } } - } - ); -} + ); + } -/** - * POST /account/profile - * Update profile information. - */ + /** + * POST /account/profile + * Update profile information. + */ -function postUpdateProfile (req, res, next) { + function postUpdateProfile (req, res, next) { + + User.findById(req.user.id, function(err) { + if (err) { return next(err); } + var errors = req.validationErrors(); + if (errors) { + req.flash('errors', errors); + return res.redirect('/account'); + } + + User.findOne({ email: req.body.email }, function(err, existingEmail) { + if (err) { + return next(err); + } + var user = req.user; + if (existingEmail && existingEmail.email !== user.email) { + req.flash('errors', { + msg: 'An account with that email address already exists.' + }); + return res.redirect('/account'); + } + User.findOne( + { 'profile.username': req.body.username }, + function(err, existingUsername) { + if (err) { + return next(err); + } + var user = req.user; + if ( + existingUsername && + existingUsername.profile.username !== user.profile.username + ) { + req.flash('errors', { + msg: 'An account with that username already exists.' + }); + return res.redirect('/account'); + } + var body = req.body || {}; + user.email = body.email.trim() || ''; + user.profile.name = body.name.trim() || ''; + user.profile.username = body.username.trim() || ''; + user.profile.location = body.location.trim() || ''; + + user.profile.githubProfile = body.githubProfile.trim() || ''; + user.profile.facebookProfile = body.facebookProfile.trim() || ''; + user.profile.linkedinProfile = body.linkedinProfile.trim() || ''; + + user.profile.codepenProfile = body.codepenProfile.trim() || ''; + user.profile.twitterHandle = body.twitterHandle.trim() || ''; + user.profile.bio = body.bio.trim() || ''; + + user.profile.picture = body.picture.trim() || + 'https://s3.amazonaws.com/freecodecamp/' + + 'camper-image-placeholder.png'; + user.portfolio.website1Title = body.website1Title.trim() || ''; + user.portfolio.website1Link = body.website1Link.trim() || ''; + user.portfolio.website1Image = body.website1Image.trim() || ''; + + user.portfolio.website2Title = body.website2Title.trim() || ''; + user.portfolio.website2Link = body.website2Link.trim() || ''; + user.portfolio.website2Image = body.website2Image.trim() || ''; + + user.portfolio.website3Title = body.website3Title.trim() || ''; + user.portfolio.website3Link = body.website3Link.trim() || ''; + user.portfolio.website3Image = body.website3Image.trim() || ''; + + + user.save(function (err) { + if (err) { + return next(err); + } + updateUserStoryPictures( + user._id.toString(), + user.profile.picture, + user.profile.username, + function(err) { + if (err) { return next(err); } + req.flash('success', { + msg: 'Profile information updated.' + }); + res.redirect('/account'); + } + ); + }); + } + ); + }); + }); + } + + /** + * POST /account/password + * Update current password. + */ + + 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); - User.findById(req.user.id, function(err) { - if (err) { return next(err); } var errors = req.validationErrors(); + if (errors) { req.flash('errors', errors); return res.redirect('/account'); } - User.findOne({ email: req.body.email }, function(err, existingEmail) { - if (err) { - return next(err); - } - var user = req.user; - if (existingEmail && existingEmail.email !== user.email) { - req.flash('errors', { - msg: 'An account with that email address already exists.' - }); - return res.redirect('/account'); - } - User.findOne( - { 'profile.username': req.body.username }, - function(err, existingUsername) { - if (err) { - return next(err); - } - var user = req.user; - if ( - existingUsername && - existingUsername.profile.username !== user.profile.username - ) { - req.flash('errors', { - msg: 'An account with that username already exists.' - }); - return res.redirect('/account'); - } - user.email = req.body.email.trim() || ''; - user.profile.name = req.body.name.trim() || ''; - user.profile.username = req.body.username.trim() || ''; - user.profile.location = req.body.location.trim() || ''; - user.profile.githubProfile = req.body.githubProfile.trim() || ''; - user.profile.facebookProfile = req.body.facebookProfile.trim() || ''; - user.profile.linkedinProfile = req.body.linkedinProfile.trim() || ''; - user.profile.codepenProfile = req.body.codepenProfile.trim() || ''; - user.profile.twitterHandle = req.body.twitterHandle.trim() || ''; - user.profile.bio = req.body.bio.trim() || ''; - - user.profile.picture = req.body.picture.trim() || - 'https://s3.amazonaws.com/freecodecamp/' + - 'camper-image-placeholder.png'; - user.portfolio.website1Title = req.body.website1Title.trim() || ''; - user.portfolio.website1Link = req.body.website1Link.trim() || ''; - user.portfolio.website1Image = req.body.website1Image.trim() || ''; - user.portfolio.website2Title = req.body.website2Title.trim() || ''; - user.portfolio.website2Link = req.body.website2Link.trim() || ''; - user.portfolio.website2Image = req.body.website2Image.trim() || ''; - user.portfolio.website3Title = req.body.website3Title.trim() || ''; - user.portfolio.website3Link = req.body.website3Link.trim() || ''; - user.portfolio.website3Image = req.body.website3Image.trim() || ''; - - - user.save(function (err) { - if (err) { - return next(err); - } - resources.updateUserStoryPictures( - user._id.toString(), - user.profile.picture, - user.profile.username, - function(err) { - if (err) { return next(err); } - req.flash('success', { - msg: 'Profile information updated.' - }); - res.redirect('/account'); - } - ); - }); - } - ); - }); - }); -} - -/** - * POST /account/password - * Update current password. - */ - -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); - - var errors = req.validationErrors(); - - if (errors) { - req.flash('errors', errors); - return res.redirect('/account'); - } - - User.findById(req.user.id, function(err, user) { - if (err) { return next(err); } - - user.password = req.body.password; - - user.save(function(err) { + User.findById(req.user.id, function(err, user) { if (err) { return next(err); } - req.flash('success', { msg: 'Password has been changed.' }); - res.redirect('/account'); - }); - }); -} + user.password = req.body.password; -/** - * POST /account/delete - * Delete user account. - */ + user.save(function(err) { + if (err) { return next(err); } -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. - */ - -function getOauthUnlink (req, res, next) { - var provider = req.params.provider; - User.findById(req.user.id, function(err, user) { - if (err) { return next(err); } - - user[provider] = null; - user.tokens = - _.reject(user.tokens, function(token) { - return token.kind === provider; - }); - - user.save(function(err) { - if (err) { return next(err); } - req.flash('info', { msg: provider + ' account has been unlinked.' }); - res.redirect('/account'); - }); - }); -} - -/** - * GET /reset/:token - * Reset Password page. - */ - -function getReset (req, res, next) { - if (req.isAuthenticated()) { - return res.redirect('/'); - } - User - .findOne({ resetPasswordToken: req.params.token }) - .where('resetPasswordExpires').gt(Date.now()) - .exec(function(err, user) { - if (err) { return next(err); } - if (!user) { - req.flash('errors', { - msg: 'Password reset token is invalid or has expired.' - }); - return res.redirect('/forgot'); - } - res.render('account/reset', { - title: 'Password Reset', - token: req.params.token + req.flash('success', { msg: 'Password has been changed.' }); + res.redirect('/account'); }); }); -} - -/** - * POST /reset/:token - * Process the reset password request. - */ - -function postReset (req, res, next) { - var errors = req.validationErrors(); - - if (errors) { - req.flash('errors', errors); - return res.redirect('back'); } - async.waterfall([ - function(done) { - User - .findOne({ resetPasswordToken: req.params.token }) - .where('resetPasswordExpires').gt(Date.now()) - .exec(function(err, user) { - if (err) { return next(err); } - if (!user) { - req.flash('errors', { - msg: 'Password reset token is invalid or has expired.' - }); - return res.redirect('back'); - } + /** + * POST /account/delete + * Delete user account. + */ - user.password = req.body.password; - user.resetPasswordToken = null; - user.resetPasswordExpires = null; + function postDeleteAccount (req, res, next) { + User.destroyById(req.user.id, function(err) { + if (err) { return next(err); } + req.logout(); + req.flash('info', { msg: 'Your account has been deleted.' }); + res.redirect('/'); + }); + } - user.save(function(err) { - if (err) { return done(err); } - req.logIn(user, function(err) { - done(err, user); - }); - }); + /** + * GET /account/unlink/:provider + * Unlink OAuth provider. + */ + + function getOauthUnlink (req, res, next) { + var provider = req.params.provider; + User.findById(req.user.id, function(err, user) { + if (err) { return next(err); } + + user[provider] = null; + user.tokens = + _.reject(user.tokens, function(token) { + return token.kind === provider; }); - }, - function(user, done) { - var transporter = nodemailer.createTransport({ - service: 'Mandrill', - auth: { - user: secrets.mandrill.user, - pass: secrets.mandrill.password - } - }); - var mailOptions = { - to: user.email, - from: 'Team@freecodecamp.com', - subject: 'Your Free Code Camp password has been changed', - text: [ - 'Hello,\n\n', - 'This email is confirming that you requested to', - 'reset your password for your Free Code Camp account.', - 'This is your email:', - user.email, - '\n' - ].join(' ') - }; - transporter.sendMail(mailOptions, function(err) { - if (err) { return done(err); } - req.flash('success', { - msg: 'Success! Your password has been changed.' - }); - done(); + + user.save(function(err) { + if (err) { return next(err); } + req.flash('info', { msg: provider + ' account has been unlinked.' }); + res.redirect('/account'); }); + }); + } + + /** + * GET /reset/:token + * Reset Password page. + */ + + function getReset (req, res, next) { + if (req.isAuthenticated()) { + return res.redirect('/'); } - ], function(err) { - if (err) { return next(err); } - res.redirect('/'); - }); -} - -/** - * GET /forgot - * Forgot Password page. - */ - -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. - */ - -function postForgot (req, res, next) { - var errors = req.validationErrors(); - - if (errors) { - req.flash('errors', errors); - return res.redirect('/forgot'); - } - - async.waterfall([ - function(done) { - crypto.randomBytes(16, function(err, buf) { - if (err) { return done(err); } - var token = buf.toString('hex'); - done(null, token); - }); - }, - function(token, done) { - User.findOne({ - email: req.body.email.toLowerCase() - }, function(err, user) { - if (err) { return done(err); } + User.findOne( + { + where: { + resetPasswordToken: req.params.token, + resetPasswordExpires: Date.now() + } + }, + function(err, user) { + if (err) { return next(err); } if (!user) { req.flash('errors', { - msg: 'No account with that email address exists.' + msg: 'Password reset token is invalid or has expired.' }); return res.redirect('/forgot'); } + res.render('account/reset', { + title: 'Password Reset', + token: req.params.token + }); + }); + } - user.resetPasswordToken = token; - // 3600000 = 1 hour - user.resetPasswordExpires = Date.now() + 3600000; + /** + * POST /reset/:token + * Process the reset password request. + */ - user.save(function(err) { + function postReset (req, res, next) { + var errors = req.validationErrors(); + + if (errors) { + req.flash('errors', errors); + return res.redirect('back'); + } + + async.waterfall([ + function(done) { + User.findOne( + { + where: { + resetPasswordToken: req.params.token, + resetPasswordExpires: Date.now() + } + }, + function(err, user) { + if (err) { return next(err); } + if (!user) { + req.flash('errors', { + msg: 'Password reset token is invalid or has expired.' + }); + return res.redirect('back'); + } + + user.password = req.body.password; + user.resetPasswordToken = null; + user.resetPasswordExpires = null; + + user.save(function(err) { + if (err) { return done(err); } + req.logIn(user, function(err) { + done(err, user); + }); + }); + }); + }, + function(user, done) { + var transporter = nodemailer.createTransport({ + service: 'Mandrill', + auth: { + user: secrets.mandrill.user, + pass: secrets.mandrill.password + } + }); + var mailOptions = { + to: user.email, + from: 'Team@freecodecamp.com', + subject: 'Your Free Code Camp password has been changed', + text: [ + 'Hello,\n\n', + 'This email is confirming that you requested to', + 'reset your password for your Free Code Camp account.', + 'This is your email:', + user.email, + '\n' + ].join(' ') + }; + transporter.sendMail(mailOptions, function(err) { if (err) { return done(err); } - done(null, token, user); + req.flash('success', { + msg: 'Success! Your password has been changed.' + }); + done(); }); - }); - }, - function(token, user, done) { - var transporter = nodemailer.createTransport({ - service: 'Mandrill', - auth: { - user: secrets.mandrill.user, - pass: secrets.mandrill.password + } + ], function(err) { + if (err) { return next(err); } + res.redirect('/'); + }); + } + + /** + * GET /forgot + * Forgot Password page. + */ + + 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. + */ + + function postForgot (req, res, next) { + var errors = req.validationErrors(); + + if (errors) { + req.flash('errors', errors); + return res.redirect('/forgot'); + } + + async.waterfall([ + function(done) { + crypto.randomBytes(16, function(err, buf) { + if (err) { return done(err); } + var token = buf.toString('hex'); + done(null, token); + }); + }, + function(token, done) { + User.findOne({ + email: req.body.email.toLowerCase() + }, function(err, user) { + if (err) { return done(err); } + if (!user) { + req.flash('errors', { + msg: 'No account with that email address exists.' + }); + return res.redirect('/forgot'); + } + + user.resetPasswordToken = token; + // 3600000 = 1 hour + user.resetPasswordExpires = Date.now() + 3600000; + + user.save(function(err) { + if (err) { return done(err); } + done(null, token, user); + }); + }); + }, + function(token, user, done) { + var transporter = nodemailer.createTransport({ + service: 'Mandrill', + auth: { + user: secrets.mandrill.user, + pass: secrets.mandrill.password + } + }); + var mailOptions = { + to: user.email, + from: 'Team@freecodecamp.com', + subject: 'Reset your Free Code Camp password', + text: [ + 'You are receiving this email because you (or someone else)\n', + 'requested we reset your Free Code Camp account\'s password.\n\n', + 'Please click on the following link, or paste this into your\n', + 'browser to complete the process:\n\n', + 'http://', + req.headers.host, + '/reset/', + token, + '\n\n', + 'If you did not request this, please ignore this email and\n', + 'your password will remain unchanged.\n' + ].join('') + }; + transporter.sendMail(mailOptions, function(err) { + if (err) { return done(err); } + req.flash('info', { + msg: 'An e-mail has been sent to ' + + user.email + + ' with further instructions.' + }); + done(null, 'done'); + }); + } + ], function(err) { + if (err) { return next(err); } + res.redirect('/forgot'); + }); + } + + function updateUserStoryPictures(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); } - }); - var mailOptions = { - to: user.email, - from: 'Team@freecodecamp.com', - subject: 'Reset your Free Code Camp password', - text: [ - 'You are receiving this email because you (or someone else)\n', - 'requested we reset your Free Code Camp account\'s password.\n\n', - 'Please click on the following link, or paste this into your\n', - 'browser to complete the process:\n\n', - 'http://', - req.headers.host, - '/reset/', - token, - '\n\n', - 'If you did not request this, please ignore this email and\n', - 'your password will remain unchanged.\n' - ].join('') - }; - transporter.sendMail(mailOptions, function(err) { - if (err) { return done(err); } - req.flash('info', { - msg: 'An e-mail has been sent to ' + - user.email + - ' with further instructions.' - }); - done(null, 'done'); + cb(); }); } - ], function(err) { - if (err) { return next(err); } - res.redirect('/forgot'); - }); -} - -module.exports = router; + } +}; diff --git a/server/boot/utility.js b/server/boot/utility.js deleted file mode 100644 index 84cb611630..0000000000 --- a/server/boot/utility.js +++ /dev/null @@ -1,446 +0,0 @@ -var express = require('express'), - async = require('async'), - moment = require('moment'), - Twit = require('twit'), - Slack = require('node-slack'), - request = require('request'), - debug = require('debug')('freecc:cntr:resources'), - constantStrings = require('../resources/constantStrings.json'), - - User = require('../../common/models/User'), - Challenge = require('../../common/models/Challenge'), - Story = require('../../common/models/Story'), - FieldGuide = require('../../common/models/FieldGuide'), - Nonprofit = require('../../common/models/Nonprofit'), - secrets = require('../../config/secrets'); - -var slack = new Slack(secrets.slackHook); -var 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://freecodecamp.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.' - ].join(''), - 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://freecodecamp.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) { - 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) { - 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) { - 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/server/config.development.js b/server/config.development.js new file mode 100644 index 0000000000..d7df882ace --- /dev/null +++ b/server/config.development.js @@ -0,0 +1,18 @@ +module.exports = { + host: '127.0.0.1', + sessionSecret: process.env.SESSION_SECRET, + + trello: { + key: process.env.TRELLO_KEY, + secret: process.env.TRELLO_SECRET + }, + + blogger: { + key: process.env.BLOGGER_KEY + }, + + github: { + clientID: process.env.GITHUB_ID, + clientSecret: process.env.GITHUB_SECRET + } +}; diff --git a/server/config.json b/server/config.json new file mode 100644 index 0000000000..8404e744fb --- /dev/null +++ b/server/config.json @@ -0,0 +1,29 @@ +{ + "restApiRoot": "/api", + "host": "0.0.0.0", + "port": 3000, + "remoting": { + "context": { + "enableHttpContext": false + }, + "rest": { + "normalizeHttpPath": false, + "xml": false + }, + "json": { + "strict": false, + "limit": "100kb" + }, + "urlencoded": { + "extended": true, + "limit": "100kb" + }, + "cors": { + "origin": true, + "credentials": true + }, + "errorHandler": { + "disableStackTrace": false + } + } +} diff --git a/server/config.local.js b/server/config.local.js new file mode 100644 index 0000000000..e5058ced89 --- /dev/null +++ b/server/config.local.js @@ -0,0 +1,5 @@ +var globalConfig = require('../common/config.global'); + +module.exports = { + restApiRoot: globalConfig.restApi +}; diff --git a/server/datasources.development.js b/server/datasources.development.js new file mode 100644 index 0000000000..f7777c5e9f --- /dev/null +++ b/server/datasources.development.js @@ -0,0 +1,6 @@ +module.exports = { + db: { + connector: 'mongodb', + url: process.env.MONGOHQ_URL + } +}; diff --git a/server/datasources.json b/server/datasources.json new file mode 100644 index 0000000000..4ce65a253f --- /dev/null +++ b/server/datasources.json @@ -0,0 +1,9 @@ +{ + "db": { + "name": "db", + "connector": "mongodb", + "host": "127.0.0.1", + "database": "foobar", + "port": 27017 + } +} diff --git a/server/middleware.json b/server/middleware.json new file mode 100644 index 0000000000..664cacda58 --- /dev/null +++ b/server/middleware.json @@ -0,0 +1,27 @@ +{ + "initial:before": { + "loopback#favicon": {} + }, + "initial": { + "compression": {} + }, + "session": { + }, + "auth": { + }, + "parse": { + }, + "routes": { + }, + "files": { + "loopback#static": { + "params": "$!../public" + } + }, + "final": { + "loopback#urlNotFound": {} + }, + "final:after": { + "errorhandler": {} + } +} diff --git a/server/model-config.json b/server/model-config.json new file mode 100644 index 0000000000..23f21c295b --- /dev/null +++ b/server/model-config.json @@ -0,0 +1,69 @@ +{ + "_meta": { + "sources": [ + "loopback/common/models", + "loopback/server/models", + "../common/models", + "./models" + ] + }, + "User": { + "dataSource": "db" + }, + "AccessToken": { + "dataSource": "db", + "public": false + }, + "ACL": { + "dataSource": "db", + "public": false + }, + "RoleMapping": { + "dataSource": "db", + "public": false + }, + "Role": { + "dataSource": "db", + "public": false + }, + "bonfire": { + "dataSource": "db", + "public": true + }, + "challenge": { + "dataSource": "db", + "public": true + }, + "comment": { + "dataSource": "db", + "public": true + }, + "fieldGuide": { + "dataSource": "db", + "public": true + }, + "job": { + "dataSource": "db", + "public": true + }, + "nonprofit": { + "dataSource": "db", + "public": true + }, + "story": { + "dataSource": "db", + "public": true + }, + "user": { + "dataSource": "db", + "public": true + }, + "userCredential": { + "dataSource": "db", + "public": true + }, + "userIdentity": { + "dataSource": "db", + "public": true + } +} diff --git a/server/passport-providers.js b/server/passport-providers.js new file mode 100644 index 0000000000..97bb12bbe6 --- /dev/null +++ b/server/passport-providers.js @@ -0,0 +1,122 @@ +var successRedirect = '/'; +var failureRedirect = '/login'; +module.exports = { + local: { + provider: 'local', + module: 'passport-local', + usernameField: 'email', + passwordField: 'password', + authPath: '/auth/local', + successRedirect: successRedirect, + failureRedirect: failureRedirect, + failureFlash: true + }, + 'facebook-login': { + provider: 'facebook', + module: 'passport-facebook', + clientID: process.env.FACEBOOK_ID, + clientSecret: process.env.FACEBOOK_SECRET, + authPath: '/auth/facebook', + callbackURL: '/auth/facebook/callback', + callbackPath: '/auth/facebook/callback', + successRedirect: successRedirect, + failureRedirect: failureRedirect, + scope: ['email'], + failureFlash: true + }, + 'facebook-link': { + provider: 'facebook', + module: 'passport-facebook', + clientID: process.env.FACEBOOK_ID, + clientSecret: process.env.FACEBOOK_SECRET, + authPath: '/link/facebook', + callbackURL: '/link/facebook/callback', + callbackPath: '/link/facebook/callback', + successRedirect: successRedirect, + failureRedirect: failureRedirect, + scope: ['email', 'user_likes'], + link: true, + failureFlash: true + }, + 'google-login': { + provider: 'google', + module: 'passport-google-oauth2', + clientID: process.env.GOOGLE_ID, + clientSecret: process.env.GOOGLE_SECRET, + authPath: '/auth/google', + callbackURL: '/auth/google/callback', + callbackPath: '/auth/google/callback', + successRedirect: successRedirect, + failureRedirect: failureRedirect, + scope: ['email', 'profile'], + failureFlash: true + }, + 'google-link': { + provider: 'google', + module: 'passport-google-oauth2', + clientID: process.env.GOOGLE_ID, + clientSecret: process.env.GOOGLE_SECRET, + authPath: '/link/google', + callbackURL: '/link/google/callback', + callbackPath: '/link/google/callback', + successRedirect: successRedirect, + failureRedirect: failureRedirect, + scope: ['email', 'profile'], + link: true, + failureFlash: true + }, + 'twitter-login': { + provider: 'twitter', + authScheme: 'oauth', + module: 'passport-twitter', + authPath: '/auth/twitter', + callbackURL: '/auth/twitter/callback', + callbackPath: '/auth/twitter/callback', + successRedirect: successRedirect, + failureRedirect: failureRedirect, + consumerKey: process.env.TWITTER_KEY, + consumerSecret: process.env.TWITTER_SECRET, + failureFlash: true + }, + 'twitter-link': { + provider: 'twitter', + authScheme: 'oauth', + module: 'passport-twitter', + authPath: '/link/twitter', + callbackURL: '/link/twitter/callback', + callbackPath: '/link/twitter/callback', + successRedirect: successRedirect, + failureRedirect: failureRedirect, + consumerKey: process.env.TWITTER_KEY, + consumerSecret: process.env.TWITTER_SECRET, + failureFlash: true + }, + 'linkedin-login': { + provider: 'linkedin', + authScheme: 'oauth', + module: 'passport-linkedin-oauth2', + authPath: '/auth/linkedin', + callbackURL: '/auth/linkedin/callback', + callbackPath: '/auth/linkedin/callback', + successRedirect: successRedirect, + failureRedirect: failureRedirect, + clientID: process.env.LINKEDIN_ID, + clientSecret: process.env.LINKEDIN_SECRET, + scope: ['r_fullprofile', 'r_emailaddress'], + failureFlash: true + }, + 'linkedin-link': { + provider: 'linkedin', + authScheme: 'oauth', + module: 'passport-linkedin-oauth2', + authPath: '/link/linkedin', + callbackURL: '/link/linkedin/callback', + callbackPath: '/link/linkedin/callback', + successRedirect: successRedirect, + failureRedirect: failureRedirect, + clientID: process.env.LINKEDIN_ID, + clientSecret: process.env.LINKEDIN_SECRET, + scope: ['r_fullprofile', 'r_emailaddress'], + failureFlash: true + } +}; diff --git a/server/server.js b/server/server.js index f5ae3b0a41..4e4c7b02bd 100755 --- a/server/server.js +++ b/server/server.js @@ -9,64 +9,38 @@ process.on('uncaughtException', function (err) { process.exit(1); // eslint-disable-line }); -var express = require('express'), - accepts = require('accepts'), - cookieParser = require('cookie-parser'), - compress = require('compression'), - session = require('express-session'), - logger = require('morgan'), - errorHandler = require('errorhandler'), - methodOverride = require('method-override'), - bodyParser = require('body-parser'), - helmet = require('helmet'), - MongoStore = require('connect-mongo')(session), - flash = require('express-flash'), - path = require('path'), - mongoose = require('mongoose'), - passport = require('passport'), - expressValidator = require('express-validator'), - // request = require('request'), - forceDomain = require('forcedomain'), - lessMiddleware = require('less-middleware'), +var R = require('ramda'), + loopback = require('loopback'), + boot = require('loopback-boot'), + accepts = require('accepts'), + cookieParser = require('cookie-parser'), + compress = require('compression'), + session = require('express-session'), + logger = require('morgan'), + errorHandler = require('errorhandler'), + methodOverride = require('method-override'), + bodyParser = require('body-parser'), + helmet = require('helmet'), + MongoStore = require('connect-mongo')(session), + flash = require('express-flash'), + path = require('path'), + expressValidator = require('express-validator'), + forceDomain = require('forcedomain'), + lessMiddleware = require('less-middleware'), - /** - * routers. - */ - homeRouter = require('./boot/home'), - userRouter = require('./boot/user'), - fieldGuideRouter = require('./boot/fieldGuide'), - challengeMapRouter = require('./boot/challengeMap'), - challengeRouter = require('./boot/challenge'), - jobsRouter = require('./boot/jobs'), - redirectsRouter = require('./boot/redirects'), - utilityRouter = require('./boot/utility'), - storyRouter = require('./boot/story'), - passportRouter = require('./boot/passport'), - - /** - * API keys and Passport configuration. - */ - secrets = require('./../config/secrets'); + passportProviders = require('./passport-providers'), + /** + * API keys and Passport configuration. + */ + secrets = require('./../config/secrets'); /** * Create Express server. */ -var app = express(); - -/** - * Connect to MongoDB. - */ -mongoose.connect(secrets.db); -mongoose.connection.on('error', function () { - console.error( - 'MongoDB Connection Error. Please make sure that MongoDB is running.' - ); -}); - -/** - * Express configuration. - */ - +var app = loopback(); +var PassportConfigurator = + require('loopback-component-passport').PassportConfigurator; +var passportConfigurator = new PassportConfigurator(app); app.set('port', process.env.PORT || 3000); app.set('views', path.join(__dirname, 'views')); @@ -101,8 +75,7 @@ app.use(session({ 'autoReconnect': true }) })); -app.use(passport.initialize()); -app.use(passport.session()); + app.use(flash()); app.disable('x-powered-by'); @@ -191,6 +164,8 @@ app.use(helmet.csp({ safari5: false })); +passportConfigurator.init(); + app.use(function (req, res, next) { // Make user object available in templates. res.locals.user = req.user; @@ -198,9 +173,14 @@ app.use(function (req, res, next) { }); app.use( - express.static(path.join(__dirname, '../public'), { maxAge: 86400000 }) + loopback.static(path.join(__dirname, '../public'), { maxAge: 86400000 }) ); +boot(app, { + appRootDir: __dirname, + dev: process.env.NODE_ENV +}); + app.use(function (req, res, next) { // Remember original destination before login. var path = req.path.split('/')[1]; @@ -213,17 +193,17 @@ app.use(function (req, res, next) { next(); }); -// add sub routers -app.use(fieldGuideRouter); -app.use(challengeMapRouter); -app.use(challengeRouter); -app.use(jobsRouter); -app.use(redirectsRouter); -app.use(utilityRouter); -app.use(storyRouter); -app.use(passportRouter); -app.use(homeRouter); -app.use(userRouter); +passportConfigurator.setupModels({ + userModel: app.models.user, + userIdentityModel: app.models.userIdentity, + userCredentialModel: app.models.userCredential +}); + +R.keys(passportProviders).map(function(strategy) { + var config = passportProviders[strategy]; + config.session = config.session !== false; + passportConfigurator.configureProvider(strategy, config); +}); /** * OAuth sign-in routes. @@ -273,12 +253,19 @@ if (process.env.NODE_ENV === 'development') { * Start Express server. */ -app.listen(app.get('port'), function () { - console.log( - 'FreeCodeCamp server listening on port %d in %s mode', - app.get('port'), - app.get('env') - ); -}); +app.start = function() { + app.listen(app.get('port'), function () { + console.log( + 'FreeCodeCamp server listening on port %d in %s mode', + app.get('port'), + app.get('env') + ); + }); +}; + +// start the server if `$ node server.js` +if (require.main === module) { + app.start(); +} module.exports = app; diff --git a/server/resources/constantStrings.json b/server/utils/constantStrings.json similarity index 100% rename from server/resources/constantStrings.json rename to server/utils/constantStrings.json diff --git a/server/resources/resources.js b/server/utils/index.js similarity index 72% rename from server/resources/resources.js rename to server/utils/index.js index 4cece5435e..a0f082bd4a 100644 --- a/server/resources/resources.js +++ b/server/utils/index.js @@ -1,18 +1,15 @@ -var async = require('async'), - path = require('path'), - // debug = require('debug')('freecc:cntr:resources'), - cheerio = require('cheerio'), - request = require('request'), - R = require('ramda'), - _ = require('lodash'), - fs = require('fs'), +var path = require('path'), + // debug = require('debug')('freecc:cntr:resources'), + cheerio = require('cheerio'), + request = require('request'), + R = require('ramda'), + _ = require('lodash'), + fs = require('fs'), - Story = require('../../common/models/Story'), - Comment = require('../../common/models/Comment'), - resources = require('./resources.json'), - nonprofits = require('../../seed_data/nonprofits.json'), - fieldGuides = require('../../seed_data/field-guides.json'); + resources = require('./resources.json'), + nonprofits = require('../../seed/nonprofits.json'), + fieldGuides = require('../../seed/field-guides.json'); /** * Cached values @@ -41,12 +38,12 @@ Array.zip = function(left, right, combinerFunction) { if (!challengeMap) { var localChallengeMap = {}; var files = fs.readdirSync( - path.join(__dirname, '../../seed_data/challenges') + path.join(__dirname, '../../seed/challenges') ); var keyCounter = 0; files = files.map(function (file) { return require( - path.join(__dirname, '../../seed_data/challenges/' + file) + path.join(__dirname, '../../seed/challenges/' + file) ); }); files = files.sort(function (a, b) { @@ -215,59 +212,5 @@ module.exports = { } }); })(); - }, - - 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/server/resources/middleware.js b/server/utils/middleware.js similarity index 100% rename from server/resources/middleware.js rename to server/utils/middleware.js diff --git a/server/resources/resources.json b/server/utils/resources.json similarity index 100% rename from server/resources/resources.json rename to server/utils/resources.json