Merge branch 'staging' of github.com:FreeCodeCamp/freecodecamp into staging

This commit is contained in:
Quincy Larson
2015-06-04 12:24:30 -07:00
84 changed files with 4154 additions and 2722 deletions

View File

@ -95,7 +95,7 @@ DEBUG=true
mongod
# Seed your database with the challenges
node seed_data/seed.js
node seed/
# start the application
gulp

7
common/config.global.js Normal file
View File

@ -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;

View File

@ -0,0 +1,16 @@
{
"name": "userCredential",
"plural": "userCredentials",
"base": "UserCredential",
"properties": {},
"validations": [],
"relations": {
"user": {
"type": "belongsTo",
"model": "user",
"foreignKey": "userId"
}
},
"acls": [],
"methods": []
}

View File

@ -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();
// }
// });
// });
//};

View File

@ -0,0 +1,16 @@
{
"name": "userIdentity",
"plural": "userIdentities",
"base": "UserIdentity",
"properties": {},
"validations": [],
"relations": {
"user": {
"type": "belongsTo",
"model": "user",
"foreignKey": "userId"
}
},
"acls": [],
"methods": []
}

19
common/models/User.js Normal file
View File

@ -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();
});
*/
};

View File

@ -18,7 +18,7 @@
"projectDescription": "string",
"logoUrl": "string",
"imageUrl": "string",
"estimatedHours": 0,
"estimatedHours": "number",
"interestedCampers": [],
"confirmedCampers": [],
"currentStatus": "string"

View File

@ -1,5 +1,5 @@
{
"name": "bonfire",
"name": "story",
"base": "PersistedModel",
"trackChanges": false,
"idInjection": true,

View File

@ -1,6 +1,6 @@
{
"name": "bonfire",
"base": "PersistedModel",
"name": "user",
"base": "User",
"trackChanges": false,
"idInjection": true,
"properties": {

19
common/screens/App.jsx Normal file
View File

@ -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 (
<div>
<Nav />
<RouteHandler />
<Footer />
</div>
);
}
});
module.exports = App;

34
common/screens/Router.jsx Normal file
View File

@ -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 = (
<Route
name='app'
path='/'
handler={ App }>
<Route
name='bonfires'
path='/bonfires/?:bonfireName?'
handler={ Bonfires } />
<DefaultRoute
handler={ Bonfires } />
</Route>
);
module.exports = function(Location) {
return Router.create({
routes: routes,
location: Location
});
};

View File

@ -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;

View File

@ -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 (
<Grid>
<Row>
<Col
xs={ 12 }
md={ 4 }>
<SidePanel
name={ name }
brief={ brief }
difficulty={ difficultyInt }
onTestBonfire={ this._onTestBonfire }
description={ description.length > 1 ? description : [] }/>
<Display
value={ display }/>
<Results
results={ results }/>
</Col>
<Col
xs={ 12 }
md={ 8 }>
<Editor
onValueChange={ BonfireActions.setUserCode }
value={ userCode }/>
</Col>
</Row>
</Grid>
);
}
});
module.exports = Bonfire;

View File

@ -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 (
<Col
xs={ 11 }
className={ classNames(textClass) }>
{ text }
</Col>
);
},
_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 (
<div key={ idx }>
<Row>
<Col
xs={ 1 }
className='text-center'>
<i className={ classNames(iconClass) }></i>
</Col>
{ this._renderText(result.text, textClass) }
{ err ? this._renderText(err, textClass) : null }
</Row>
<div className='ten-pixel-break'></div>
</div>
);
}.bind(this));
},
render: function() {
var results = this.props.results;
if (!results || results.length && results.length === 0) {
return null;
}
return (
<Grid>
{ this._renderResult(this.props.results) }
</Grid>
);
}
});
module.exports = Results;

View File

@ -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 (
<i
key={ num }
className={ className }/>
);
});
},
_renderMoreInfo: function(isDescription) {
var description = this.props.description.map((sentance, index) => {
return <p key={ index }>{ sentance }</p>;
});
if (isDescription && this.state.isMoreInfoOpen) {
return (
<Row>
<Col xs={ 12 }>
{ description }
</Col>
</Row>
);
}
return null;
},
_renderMoreInfoButton: function(isDescription) {
if (isDescription) {
return (
<Button
onClick={ this._toggleMoreInfo }
bsStyle='primary'
block={ true }
className='btn-primary-ghost'>
<span className='ion-arrow-down-b'></span>
More information
</Button>
);
}
return null;
},
render: function() {
var isDescription = this.props.description &&
this.props.description.length > 1;
return (
<div>
<h1 className='text-center'>{ this.props.name }</h1>
<h2 className='text-center'>
<div className='bonfire-flames'>
Difficulty:&nbsp;
{ this._renderFlames() }
</div>
</h2>
<Well>
<Row>
<Col xs={ 12 }>
<div className='bonfire-instructions'>
<p>{ this.props.brief }</p>
<div>
{ this._renderMoreInfo(isDescription) }
{ this._renderMoreInfoButton(isDescription) }
</div>
</div>
</Col>
</Row>
</Well>
<Button
bsStyle='primary'
block={ true }
className='btn-big'
onClick={ this.props.onTestBonfire }>
Run Code (ctrl + enter)
</Button>
<br />
</div>
);
}
});
module.exports = SidePanel;

View File

@ -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;

View File

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

View File

@ -0,0 +1 @@
module.exports = require('./Bonfires.jsx');

View File

@ -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;

View File

@ -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;

View File

@ -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 (
<form className='code'>
<div className='form-group codeMirrorView'>
<Tailspin
{ ...options }
config={ config }/>
</div>
</form>
);
}
});
module.exports = Editor;

View File

@ -0,0 +1 @@
module.exports = require('./Display.jsx');

View File

@ -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 (
<div id='mainEditorPanel'>
<form className='code'>
<div className='form-group codeMirrorView'>
<Tailspin
{ ...options }
config={ config }/>
</div>
</form>
</div>
);
}
});
module.exports = Editor;

View File

@ -0,0 +1 @@
module.exports = require('./Editor.jsx');

View File

@ -0,0 +1,106 @@
var React = require('react');
var Footer = React.createClass({
render: function() {
return (
<div className='fcc-footer'>
<div className='col-xs-12 hidden-xs hidden-sm'>
<a
href='http://blog.freecodecamp.com'
target='_blank' className='ion-speakerphone'>
&nbsp;Blog&nbsp;&nbsp;
</a>
<a
ref='http://www.twitch.tv/freecodecamp'
target='_blank' className='ion-social-twitch-outline'>
&nbsp;Twitch&nbsp;&nbsp;
</a>
<a
href='http://github.com/freecodecamp'
target='_blank'
className='ion-social-github'>
&nbsp;Github&nbsp;&nbsp;
</a>
<a
href='http://twitter.com/freecodecamp'
target='_blank' className='ion-social-twitter'>
&nbsp;Twitter&nbsp;&nbsp;
</a>
<a
href='http://facebook.com/freecodecamp'
target='_blank'
className='ion-social-facebook'>
&nbsp;Facebook&nbsp;&nbsp;
</a>
<a
ref='/learn-to-code'
className='ion-information-circled'>
&nbsp;About&nbsp;&nbsp;
</a>
<a
href='/privacy'
className='ion-locked'>
&nbsp;Privacy&nbsp;&nbsp;
</a>
</div>
<div className='col-xs-12 visible-xs visible-sm'>
<a
href='http://blog.freecodecamp.com'
target='_blank' className='ion-speakerphone'>
<span className='sr-only'>
Free Code Camp\'s Blog
</span>
</a>
<a
href='http://www.twitch.tv/freecodecamp'
target='_blank'
className='ion-social-twitch-outline'>
<span className='sr-only'>
Free Code Camp Live Pair Programming on Twitch.tv
</span>
</a>
<a
href='http://github.com/freecodecamp'
target='_blank'
className='ion-social-github'>
<span className='sr-only'>
Free Code Camp on GitHub
</span>
</a>
<a
href='http://twitter.com/freecodecamp'
target='_blank'
className='ion-social-twitter'>
<span className='sr-only'>
Free Code Camp on Twitter
</span>
</a>
<a
href='http://facebook.com/freecodecamp'
target='_blank'
className='ion-social-facebook'>
<span className='sr-only'>
Free Code Camp on Facebook
</span>
</a>
<a
href='/learn-to-code'
className='ion-information-circled'>
<span className='sr-only'>
About Free Code Camp
</span>
</a>
<a
href='/privacy'
className='ion-locked'>
<span className='sr-only'>
Free Code Camp's Privacy Policy
</span>
</a>
</div>
</div>
);
}
});
module.exports = Footer;

View File

@ -0,0 +1 @@
module.exports = require('./Footer.jsx');

View File

@ -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 (
<a href='/'>
<img
src={ fCClogo }
alt='learn to code javascript at Free Code Camp logo'
className='img-responsive nav-logo' />
</a>
);
},
_renderSignin: function() {
if (this.props.signedIn) {
return (
<NavItem
eventKey={ 2 }>
Show Picture
</NavItem>
);
} else {
return (
<NavItemFCC
eventKey={ 2 }
href='/login'
aClassName='btn signup-btn signup-btn-nav'>
Sign In
</NavItemFCC>
);
}
},
render: function() {
return (
<Navbar
brand={ this._renderBrand() }
fixedTop={ true }
toggleNavKey={ 0 }
className='nav-height'>
<Nav
right={ true }
eventKey={ 0 }
className='hamburger-dropdown'>
<NavItem
eventKey={ 1 }
href='/Challenges'>
Challenges
</NavItem>
<NavItem
eventKey={ 1 }
href='Chat'>
Chat
</NavItem>
<NavItem
eventKey={ 2 }
href='/bonfires'>
Bonfires
</NavItem>
{ this._renderSignin() }
</Nav>
</Navbar>
);
}
});
module.exports = NavBarComp;

View File

@ -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 (
<li {...props} className={joinClasses(props.className, classSet(classes))}>
<a
href={href}
title={title}
target={target}
className={ this.props.aClassName }
onClick={this.handleClick}
ref="anchor">
{ children }
</a>
</li>
);
},
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;

View File

@ -0,0 +1 @@
module.exports = require('./Nav.jsx');

View File

@ -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"

View File

@ -237,7 +237,7 @@
"name": "Waypoint: Fill in the Blank with Placeholder Text",
"difficulty": 0.015,
"description": [
"Replace the text inside your <code>p</code> element with the first few words of the provided \"Ktty Ipsum\" text.",
"Replace the text inside your <code>p</code> 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\"!",

View File

@ -683,7 +683,7 @@
" <li>Fork the Free Code Camp repository and <code>open seed_data/bonfires.json</code> to become familiar with the format of our bonfires.</li>",
" <li>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.</li>",
" <li>In the terminal, run <code>node seed_data/seed.js</code>. Run <code>gulp</code>. 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.</li>",
" <li>Solved your own Bonfire. Confirmed that your tests work as expected and that your instructions are sufficiently clear.</li>",
" <li>Solve your own Bonfire. Confirm that your tests work as expected and that your instructions are sufficiently clear.</li>",
" <li>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.</li>",
" </ol>",
" </p>",

99
seed/index.js Normal file
View File

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

View File

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

View File

@ -0,0 +1,4 @@
module.exports = function enableAuthentication(app) {
// enable authentication
app.enableAuth();
};

View File

@ -31,16 +31,32 @@
*/
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();
function getMDNlinks(links) {
// takes in an array of links, which are strings
var populatedLinks = [];
// for each key value, push the corresponding link
// from the MDNlinks object into a new array
if (links) {
links.forEach(function (value) {
populatedLinks.push(MDNlinks[value]);
});
}
return populatedLinks;
}
module.exports = function(app) {
var router = app.loopback.Router();
var Challenge = app.models.Challenge;
var User = app.models.User;
router.get(
'/challenges/next-challenge',
@ -59,19 +75,7 @@ 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
var populatedLinks = [];
// for each key value, push the corresponding link
// from the MDNlinks object into a new array
if (links) {
links.forEach(function (value) {
populatedLinks.push(MDNlinks[value]);
});
}
return populatedLinks;
}
app.use(router);
function returnNextChallenge(req, res, next) {
if (!req.user) {
@ -81,7 +85,7 @@ function returnNextChallenge(req, res, next) {
return elem._id;
});
req.user.uncompletedChallenges = resources.allChallengeIds()
req.user.uncompletedChallenges = utils.allChallengeIds()
.filter(function (elem) {
if (completed.indexOf(elem) === -1) {
return elem;
@ -135,7 +139,7 @@ function returnCurrentChallenge(req, res, next) {
return elem._id;
});
req.user.uncompletedChallenges = resources.allChallengeIds()
req.user.uncompletedChallenges = utils.allChallengeIds()
.filter(function (elem) {
if (completed.indexOf(elem) === -1) {
return elem;
@ -176,11 +180,11 @@ function returnIndividualChallenge(req, res, next) {
.join(' ') :
dashedName.replace(/\-/g, ' ');
Challenge.find({'name': new RegExp(challengeName, 'i')},
Challenge.find(
{ where: { name: new RegExp(challengeName, 'i') } },
function(err, challengeFromMongo) {
if (err) {
return next(err);
}
if (err) { return next(err); }
// Handle not found
if (challengeFromMongo.length < 1) {
req.flash('errors', {
@ -224,11 +228,11 @@ function returnIndividualChallenge(req, res, next) {
details: challenge.description.slice(1),
tests: challenge.tests,
challengeSeed: challenge.challengeSeed,
verb: resources.randomVerb(),
phrase: resources.randomPhrase(),
compliment: resources.randomCompliment(),
verb: utils.randomVerb(),
phrase: utils.randomPhrase(),
compliment: utils.randomCompliment(),
challengeId: challenge._id,
environment: resources.whichEnvironment(),
environment: utils.whichEnvironment(),
challengeType: challenge.challengeType
});
},
@ -242,9 +246,9 @@ function returnIndividualChallenge(req, res, next) {
details: challenge.description.slice(1),
tests: challenge.tests,
challengeSeed: challenge.challengeSeed,
verb: resources.randomVerb(),
phrase: resources.randomPhrase(),
compliment: resources.randomCompliment(),
verb: utils.randomVerb(),
phrase: utils.randomPhrase(),
compliment: utils.randomCompliment(),
challengeId: challenge._id,
challengeType: challenge.challengeType
});
@ -258,9 +262,9 @@ function returnIndividualChallenge(req, res, next) {
details: challenge.description,
tests: challenge.tests,
video: challenge.challengeSeed[0],
verb: resources.randomVerb(),
phrase: resources.randomPhrase(),
compliment: resources.randomCompliment(),
verb: utils.randomVerb(),
phrase: utils.randomPhrase(),
compliment: utils.randomCompliment(),
challengeId: challenge._id,
challengeType: challenge.challengeType
});
@ -273,9 +277,9 @@ function returnIndividualChallenge(req, res, next) {
name: challenge.name,
details: challenge.description,
video: challenge.challengeSeed[0],
verb: resources.randomVerb(),
phrase: resources.randomPhrase(),
compliment: resources.randomCompliment(),
verb: utils.randomVerb(),
phrase: utils.randomPhrase(),
compliment: utils.randomCompliment(),
challengeId: challenge._id,
challengeType: challenge.challengeType
});
@ -288,9 +292,9 @@ function returnIndividualChallenge(req, res, next) {
name: challenge.name,
details: challenge.description,
video: challenge.challengeSeed[0],
verb: resources.randomVerb(),
phrase: resources.randomPhrase(),
compliment: resources.randomCompliment(),
verb: utils.randomVerb(),
phrase: utils.randomPhrase(),
compliment: utils.randomCompliment(),
challengeId: challenge._id,
challengeType: challenge.challengeType
});
@ -307,9 +311,9 @@ function returnIndividualChallenge(req, res, next) {
details: challenge.description,
tests: challenge.tests,
challengeSeed: challenge.challengeSeed,
verb: resources.randomVerb(),
phrase: resources.randomPhrase(),
compliment: resources.randomCompliment(),
verb: utils.randomVerb(),
phrase: utils.randomPhrase(),
compliment: utils.randomCompliment(),
bonfires: challenge,
challengeId: challenge._id,
MDNkeys: challenge.MDNlinks,
@ -339,12 +343,12 @@ function completedBonfire(req, res, next) {
var challengeName = req.body.challengeInfo.challengeName;
if (isCompletedWith) {
var paired = User.find({'profile.username': isCompletedWith.toLowerCase()})
.limit(1);
paired.exec(function (err, pairedWith) {
if (err) {
return next(err);
} else {
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);
@ -388,11 +392,9 @@ function completedBonfire(req, res, next) {
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,7 +408,6 @@ function completedBonfire(req, res, next) {
res.send(true);
}
});
}
});
} else {
req.user.completedChallenges.push({
@ -425,14 +426,9 @@ function completedBonfire(req, res, next) {
req.user.uncompletedChallenges.splice(index, 1);
}
req.user.save(function (err, user) {
if (err) {
return next(err);
}
// NOTE(berks): Under certain conditions the res is never ended
if (user) {
req.user.save(function (err) {
if (err) { return next(err); }
res.send(true);
}
});
}
}
@ -486,13 +482,11 @@ function completedZiplineOrBasejump(req, res, next) {
}
if (isCompletedWith) {
var paired = User.find({'profile.username': isCompletedWith.toLowerCase()})
.limit(1);
paired.exec(function (err, pairedWithFromMongo) {
if (err) {
return next(err);
} else {
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,7 +537,6 @@ function completedZiplineOrBasejump(req, res, next) {
}
});
});
}
});
} else {
@ -570,12 +561,12 @@ function completedZiplineOrBasejump(req, res, next) {
if (err) {
return next(err);
}
// NOTE(berks): under certain conditions this will not close the response.
// NOTE(berks): under certain conditions this will not close
// the response.
if (user) {
return res.sendStatus(200);
}
});
}
}
module.exports = router;
};

View File

@ -1,21 +1,23 @@
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');
module.exports = function(app) {
var User = app.models.User;
var router = app.loopback.Router();
router.get('/map', middleware.userMigration, challengeMap);
router.get('/learn-to-code', function(req, res) {
res.redirect(301, '/map');
});
router.get('/about', function(req, res) {
res.redirect(301, '/map');
});
app.use(router);
function challengeMap(req, res, next) {
var completedList = [];
@ -29,7 +31,7 @@ function challengeMap(req, res, next) {
.map(function(challenge) {
return challenge._id;
});
var challengeList = resources.
var challengeList = utils.
getChallengeMapForDisplay(completedChallengeList);
Object.keys(challengeList).forEach(function(key) {
@ -48,10 +50,9 @@ function challengeMap(req, res, next) {
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);
}
User.count(function(err, camperCount) {
if (err) { return next(err); }
res.render('challengeMap/show', {
daysRunning: daysRunning,
camperCount: numberWithCommas(camperCount),
@ -61,5 +62,4 @@ function challengeMap(req, res, next) {
});
});
}
module.exports = router;
};

27
server/boot/explorer.js Normal file
View File

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

View File

@ -1,22 +1,25 @@
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);
app.use(router);
function returnIndividualFieldGuide(req, res, next) {
var dashedName = req.params.fieldGuideName;
if (req.user) {
var completed = req.user.completedFieldGuides;
var uncompletedFieldGuides = resources.allFieldGuideIds()
var uncompletedFieldGuides = utils.allFieldGuideIds()
.filter(function (elem) {
if (completed.indexOf(elem) === -1) {
return elem;
@ -24,9 +27,12 @@ function returnIndividualFieldGuide(req, res, next) {
});
req.user.uncompletedFieldGuides = uncompletedFieldGuides;
// TODO(berks): handle callback properly
req.user.save();
req.user.save(function(err) {
if (err) { return next(err); }
});
}
// NOTE(berks): loopback might have issue with regex here.
FieldGuide.find(
{ dashedName: new RegExp(dashedName, 'i') },
function(err, fieldGuideFromMongo) {
@ -59,7 +65,7 @@ function returnIndividualFieldGuide(req, res, next) {
}
function showAllFieldGuides(req, res) {
var allFieldGuideNamesAndIds = resources.allFieldGuideNamesAndIds();
var allFieldGuideNamesAndIds = utils.allFieldGuideNamesAndIds();
var completedFieldGuides = [];
if (req.user && req.user.completedFieldGuides) {
@ -90,7 +96,7 @@ function returnNextFieldGuide(req, res, next) {
"You've read all our current Field Guide entries. You can ",
'contribute to our Field Guide ',
"<a href='https://github.com/FreeCodeCamp/freecodecamp/blob/",
"staging/seed_data/field-guides.json'>here</a>."
"staging/seed/field-guides.json'>here</a>."
].join('')
});
}
@ -119,5 +125,4 @@ function completedFieldGuide(req, res, next) {
res.send(true);
});
}
module.exports = router;
};

View File

@ -1,10 +1,12 @@
var express = require('express');
var router = express.Router();
var message =
'Learn to Code JavaScript and get a Coding Job by Helping Nonprofits';
module.exports = function(app) {
var router = app.loopback.Router();
router.get('/', index);
app.use(router);
function index(req, res, next) {
if (req.user && !req.user.profile.picture) {
req.user.profile.picture =
@ -18,5 +20,4 @@ function index(req, res, next) {
res.render('home', { title: message });
}
}
module.exports = router;
};

View File

@ -1,8 +1,9 @@
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);
app.use(router);
function jobsDirectory(req, res, next) {
Job.find({}, function(err, jobs) {
@ -14,5 +15,4 @@ function jobsDirectory(req, res, next) {
});
});
}
module.exports = router;
};

View File

View File

@ -1,20 +1,25 @@
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);
app.use(router);
function nonprofitsDirectory(req, res, next) {
Nonprofit.find({ estimatedHours: { $gt: 0 } }, function(err, nonprofits) {
Nonprofit.find(
{ where: { estimatedHours: { $gt: 0 } } },
function(err, nonprofits) {
if (err) { return next(err); }
res.render('nonprofits/directory', {
title: 'Nonprofits we help',
nonprofits: nonprofits
});
});
}
);
}
function returnIndividualNonprofit(req, res, next) {
@ -22,7 +27,7 @@ function returnIndividualNonprofit(req, res, next) {
var nonprofitName = dashedName.replace(/\-/g, ' ');
Nonprofit.find(
{ name: new RegExp(nonprofitName, 'i') },
{ where: { name: new RegExp(nonprofitName, 'i') } },
function(err, nonprofit) {
if (err) {
return next(err);
@ -122,5 +127,4 @@ function interestedInNonprofit(req, res, next) {
}
}
*/
module.exports = router;
};

View File

@ -1,8 +1,9 @@
var express = require('express'),
passport = require('passport'),
/*
var passport = require('passport'),
passportConf = require('../../config/passport');
var router = express.Router();
module.exports = function(app) {
var router = app.loopback.Router();
var passportOptions = {
successRedirect: '/',
failureRedirect: '/login'
@ -65,4 +66,6 @@ router.get(
}
);
module.exports = router;
app.use(router);
};
*/

494
server/boot/randomAPIs.js Normal file
View File

@ -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 <a href=\'mailto:team@' +
'freecodecamp.com?subject=' +
'slack%20invite%20failed%20to%20send\'>' +
'email us</a>.'
});
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'
}
]
);
}
};

View File

@ -1,5 +1,5 @@
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(
@ -41,7 +41,10 @@ router.get('/nodeschool-challenges', function(req, res) {
});
router.get('/privacy', function(req, res) {
res.redirect(301, '/field-guide/what-is-the-free-code-camp-privacy-policy?');
res.redirect(
301, '/field-guide/what-is-the-free-code-camp-privacy-policy?'
);
});
module.exports = router;
app.use(router);
};

4
server/boot/restApi.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = function mountRestApi(app) {
var restApiRoot = app.get('restApiRoot');
app.use(restApiRoot, app.loopback.rest());
};

View File

@ -1,16 +1,16 @@
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');
module.exports = function(app) {
var router = app.loopback.Router();
var User = app.models.User;
var Story = app.models.Story;
router.get('/stories/hotStories', hotJSON);
router.get('/stories/recentStories', recentJSON);
@ -27,6 +27,8 @@ router.post('/stories/search', getStories);
router.get('/news/:storyName', returnIndividualStory);
router.post('/stories/upvote/', upvote);
app.use(router);
function hotRank(timeValue, rank) {
/*
* Hotness ranking algorithm: http://amix.dk/blog/post/19588
@ -39,7 +41,6 @@ function hotRank(timeValue, rank) {
var z = Math.log(rank) / Math.log(10);
hotness = z + (timeValue / time48Hours);
return hotness;
}
function hotJSON(req, res, next) {
@ -252,9 +253,12 @@ function upvote(req, res, next) {
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'
// 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) {
User.findOne(
{ where: { id: story.author.userId } },
function(err, user) {
if (err) { return next(err); }
user.progressTimestamps.push(Date.now() || 0);
@ -267,14 +271,17 @@ function upvote(req, res, next) {
return next(err);
}
});
});
}
);
return res.send(story);
});
}
function comments(req, res, next) {
var data = req.params.id;
Comment.find({'_id': data}, function(err, comment) {
Comment.find(
{ where: {'_id': data } },
function(err, comment) {
if (err) {
return next(err);
}
@ -305,7 +312,9 @@ function newStory(req, res, next) {
if (url.search(/^https?:\/\//g) === -1) {
url = 'http://' + url;
}
Story.find({'link': url}, function(err, story) {
Story.find(
{ where: {'link': url} },
function(err, story) {
if (err) {
return next(err);
}
@ -318,8 +327,9 @@ function newStory(req, res, next) {
storyURL: '/news/' + story.pop().storyLink
});
}
resources.getURLTitle(url, processResponse);
});
utils.getURLTitle(url, processResponse);
}
);
function processResponse(err, story) {
if (err) {
@ -360,7 +370,7 @@ function storySubmission(req, res, next) {
}
Story.count({
storyLink: new RegExp('^' + storyLink + '(?: [0-9]+)?$', 'i')
storyLink: { like: new RegExp('^' + storyLink + '(?: [0-9]+)?$', 'i') }
}, function (err, storyCount) {
if (err) {
return next(err);
@ -461,17 +471,21 @@ function commentOnCommentSubmit(req, res, next) {
return next(new Error('Not authorized'));
}
var sanitizedBody = sanitizeHtml(data.body,
var sanitizedBody = sanitizeHtml(
data.body,
{
allowedTags: [],
allowedAttributes: []
}).replace(/&quot;/g, '"');
}
).replace(/&quot;/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,
@ -494,7 +508,7 @@ function commentOnCommentSubmit(req, res, next) {
function commentEdit(req, res, next) {
Comment.find({'_id': req.params.id}, function(err, cmt) {
Comment.find({ id: req.params.id }, function(err, cmt) {
if (err) {
return next(err);
}
@ -504,7 +518,6 @@ function commentEdit(req, res, next) {
return next(new Error('Not authorized'));
}
var sanitizedBody = sanitizeHtml(req.body.body, {
allowedTags: [],
allowedAttributes: []
@ -538,7 +551,7 @@ function commentSave(comment, Context, res, next) {
// Based on the context retrieve the parent
// object of the comment (Story/Comment)
Context.find({
'_id': data.associatedPost
id: data.associatedPost
}, function (err, associatedContext) {
if (err) {
return next(err);
@ -605,5 +618,4 @@ function commentSave(comment, Context, res, next) {
}
});
}
module.exports = router;
};

View File

@ -3,16 +3,17 @@ 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');
module.exports = function(app) {
var router = app.loopback.Router();
var User = app.models.User;
var Story = app.models.Story;
var Comment = app.models.Comment;
var router = express.Router();
router.get('/login', function(req, res) {
res.redirect(301, '/signin');
});
@ -20,7 +21,7 @@ router.get('/logout', function(req, res) {
res.redirect(301, '/signout');
});
router.get('/signin', getSignin);
router.post('/signin', postSignin);
// router.post('/signin', postSignin);
router.get('/signout', signout);
router.get('/forgot', getForgot);
router.post('/forgot', postForgot);
@ -29,7 +30,7 @@ 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.post('/email-signin', postSignin);
router.get('/account/api', getAccountAngular);
router.get('/api/checkUniqueUsername/:username', checkUniqueUsername);
router.get('/api/checkExistingUsername/:username', checkExistingUsername);
@ -42,6 +43,8 @@ router.get('/account', getAccount);
// Ensure this is the last route!
router.get('/:username', returnUser);
app.use(router);
/**
* GET /signin
* Siginin page.
@ -61,6 +64,8 @@ function getSignin (req, res) {
* 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();
@ -95,6 +100,7 @@ function postSignin (req, res, next) {
});
})(req, res, next);
}
*/
/**
* GET /signout
@ -325,8 +331,8 @@ function returnUser (req, res, next) {
}
if (user[0]) {
user = user[0];
user.progressTimestamps = user.progressTimestamps.sort(function(a, b) {
user.progressTimestamps =
user.progressTimestamps.sort(function(a, b) {
return a - b;
});
@ -486,36 +492,41 @@ function postUpdateProfile (req, res, next) {
});
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() || '';
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.picture = req.body.picture.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 = 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.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);
}
resources.updateUserStoryPictures(
updateUserStoryPictures(
user._id.toString(),
user.profile.picture,
user.profile.username,
@ -540,7 +551,9 @@ function postUpdateProfile (req, res, next) {
*/
function postUpdatePassword (req, res, next) {
req.assert('password', 'Password must be at least 4 characters long').len(4);
req.assert('password', 'Password must be at least 4 characters long')
.len(4);
req.assert('confirmPassword', 'Passwords do not match')
.equals(req.body.password);
@ -571,7 +584,7 @@ function postUpdatePassword (req, res, next) {
*/
function postDeleteAccount (req, res, next) {
User.remove({ _id: req.user.id }, function(err) {
User.destroyById(req.user.id, function(err) {
if (err) { return next(err); }
req.logout();
req.flash('info', { msg: 'Your account has been deleted.' });
@ -612,10 +625,14 @@ 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) {
User.findOne(
{
where: {
resetPasswordToken: req.params.token,
resetPasswordExpires: Date.now()
}
},
function(err, user) {
if (err) { return next(err); }
if (!user) {
req.flash('errors', {
@ -645,10 +662,14 @@ function postReset (req, res, next) {
async.waterfall([
function(done) {
User
.findOne({ resetPasswordToken: req.params.token })
.where('resetPasswordExpires').gt(Date.now())
.exec(function(err, user) {
User.findOne(
{
where: {
resetPasswordToken: req.params.token,
resetPasswordExpires: Date.now()
}
},
function(err, user) {
if (err) { return next(err); }
if (!user) {
req.flash('errors', {
@ -803,4 +824,58 @@ function postForgot (req, res, next) {
});
}
module.exports = router;
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);
}
cb();
});
}
}
};

View File

@ -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 <a href=\'mailto:team@' +
'freecodecamp.com?subject=' +
'slack%20invite%20failed%20to%20send\'>' +
'email us</a>.'
});
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;

View File

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

29
server/config.json Normal file
View File

@ -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
}
}
}

5
server/config.local.js Normal file
View File

@ -0,0 +1,5 @@
var globalConfig = require('../common/config.global');
module.exports = {
restApiRoot: globalConfig.restApi
};

View File

@ -0,0 +1,6 @@
module.exports = {
db: {
connector: 'mongodb',
url: process.env.MONGOHQ_URL
}
};

9
server/datasources.json Normal file
View File

@ -0,0 +1,9 @@
{
"db": {
"name": "db",
"connector": "mongodb",
"host": "127.0.0.1",
"database": "foobar",
"port": 27017
}
}

27
server/middleware.json Normal file
View File

@ -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": {}
}
}

69
server/model-config.json Normal file
View File

@ -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
}
}

View File

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

View File

@ -9,7 +9,9 @@ process.on('uncaughtException', function (err) {
process.exit(1); // eslint-disable-line
});
var express = require('express'),
var R = require('ramda'),
loopback = require('loopback'),
boot = require('loopback-boot'),
accepts = require('accepts'),
cookieParser = require('cookie-parser'),
compress = require('compression'),
@ -22,27 +24,11 @@ var express = require('express'),
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'),
/**
* 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'),
passportProviders = require('./passport-providers'),
/**
* API keys and Passport configuration.
*/
@ -51,22 +37,10 @@ var express = require('express'),
/**
* 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,6 +253,7 @@ if (process.env.NODE_ENV === 'development') {
* Start Express server.
*/
app.start = function() {
app.listen(app.get('port'), function () {
console.log(
'FreeCodeCamp server listening on port %d in %s mode',
@ -280,5 +261,11 @@ app.listen(app.get('port'), function () {
app.get('env')
);
});
};
// start the server if `$ node server.js`
if (require.main === module) {
app.start();
}
module.exports = app;

View File

@ -1,5 +1,4 @@
var async = require('async'),
path = require('path'),
var path = require('path'),
// debug = require('debug')('freecc:cntr:resources'),
cheerio = require('cheerio'),
request = require('request'),
@ -8,11 +7,9 @@ var async = require('async'),
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');
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();
});
}
}
};