committed by
Mrugesh Mohapatra
parent
61cd404c41
commit
644f34d2ad
@ -95,6 +95,7 @@ exports.createPages = ({ graphql, boundActionCreators }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const webpack = require('webpack');
|
||||||
const generateBabelConfig = require('gatsby/dist/utils/babel-config');
|
const generateBabelConfig = require('gatsby/dist/utils/babel-config');
|
||||||
|
|
||||||
exports.modifyWebpackConfig = ({ config, stage }) => {
|
exports.modifyWebpackConfig = ({ config, stage }) => {
|
||||||
@ -123,5 +124,12 @@ exports.modifyWebpackConfig = ({ config, stage }) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
config.plugin('DefinePlugin', webpack.DefinePlugin, [
|
||||||
|
{
|
||||||
|
AUTH0_DOMAIN: JSON.stringify(process.env.AUTH0_DOMAIN),
|
||||||
|
AUTH0_CLIENT_ID: JSON.stringify(process.env.AUTH0_CLIENT_ID),
|
||||||
|
AUTH0_NAMESPACE: JSON.stringify(process.env.AUTH0_NAMESPACE)
|
||||||
|
}
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
12559
packages/learn/package-lock.json
generated
12559
packages/learn/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@
|
|||||||
"author": "Kyle Mathews <mathews.kyle@gmail.com>",
|
"author": "Kyle Mathews <mathews.kyle@gmail.com>",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adler32": "^0.1.7",
|
"adler32": "^0.1.7",
|
||||||
|
"auth0-js": "^9.5.1",
|
||||||
"babel-core": "^6.26.0",
|
"babel-core": "^6.26.0",
|
||||||
"babel-jest": "^22.4.3",
|
"babel-jest": "^22.4.3",
|
||||||
"babel-standalone": "^6.26.0",
|
"babel-standalone": "^6.26.0",
|
||||||
|
@ -1 +1,3 @@
|
|||||||
MONGOHQ_URL='mongodb://localhost:27017/freecodecamp'
|
AUTH0_DOMAIN=<auth0-tennant>.auth0.com
|
||||||
|
AUTH0_CLIENT_ID=this-is-me
|
||||||
|
AUTH0_NAMESPACE='https://auth-ns.freecodecamp.org/'
|
@ -0,0 +1,655 @@
|
|||||||
|
{
|
||||||
|
"name": "Advanced Node and Express",
|
||||||
|
"order": 3,
|
||||||
|
"time": "5 hours",
|
||||||
|
"helpRoom": "Help",
|
||||||
|
"challenges": [
|
||||||
|
{
|
||||||
|
"id": "5895f700f9fc0f352b528e63",
|
||||||
|
"title": "Set up a Template Engine",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"A template engine enables you to use static template files (such as those written in <em>Pug</em>) in your app. At runtime, the template engine replaces variables in a template file with actual values which can be supplied by your server, and transforms the template into a static HTML file that is then sent to the client. This approach makes it easier to design an HTML page and allows for displaying of variables on the page without needing to make an API call from the client.",
|
||||||
|
"To set up <em>Pug</em> for use in your project, you will need to add it as a dependency first in your package.json. <code>\"pug\": \"^0.1.0\"</code>",
|
||||||
|
"Now to tell Node/Express to use the templating engine you will have to tell your express <b>app</b> to <b>set</b> 'pug' as the 'view-engine'. <code>app.set('view engine', 'pug')</code>",
|
||||||
|
"Lastly, you should change your response to the request for the index route to <code>res.render</code> with the path to the view <em>views/pug/index.pug</em>.",
|
||||||
|
"If all went as planned, you should refresh your apps home page and see a small message saying you're successfully rending the Pug from our Pug file! Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Pug is a dependency",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/package.json') .then(data => { var packJson = JSON.parse(data); assert.property(packJson.dependencies, 'pug', 'Your project should list \"pug\" as a dependency'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "View engine is Pug",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /('|\")view engine('|\"),( |)('|\")pug('|\")/gi, 'Your project should set Pug as a view engine'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Pug is working",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/') .then(data => { assert.match(data, /pug-success-message/gi, 'Your projects home page should now be rendered by pug with the projects .pug file unaltered'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5895f70bf9fc0f352b528e64",
|
||||||
|
"title": "Use a Template Engine's Powers",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"One of the greatest features of using a template engine is being able to pass variables from the server to the template file before rendering it to HTML.",
|
||||||
|
"In your Pug file, you're about to use a variable by referencing the variable name as <code>#{variable_name}</code> inline with other text on an element or by using an equal side on the element without a space such as <code>p= variable_name</code> which sets that p elements text to equal the variable.",
|
||||||
|
"We strongly recommend looking at the syntax and structure of Pug <a href='https://github.com/pugjs/pug'>here</a> on their Githubs README. Pug is all about using whitespace and tabs to show nested elements and cutting down on the amount of code needed to make a beautiful site.",
|
||||||
|
"Looking at our pug file 'index.pug' included in your project, we used the variables <em>title</em> and <em>message</em>",
|
||||||
|
"To pass those alone from our server, you will need to add an object as a second argument to your <em>res.render</em> with the variables and their value. For example, pass this object along setting the variables for your index view: <code>{title: 'Hello', message: 'Please login'</code>",
|
||||||
|
"It should look like: <code>res.render(process.cwd() + '/views/pug/index', {title: 'Hello', message: 'Please login'});</code>",
|
||||||
|
"Now refresh your page and you should see those values rendered in your view in the correct spot as layed out in your index.pug file! Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Pug render variables correct",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/') .then(data => { assert.match(data, /pug-variable(\"|')>Please login/gi, 'Your projects home page should now be rendered by pug with the projects .pug file unaltered'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5895f70cf9fc0f352b528e65",
|
||||||
|
"title": "Set up Passport",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"It's time to set up <em>Passport</em> so we can finally start allowing a user to register or login to an account! In addition to Passport, we will use Express-session to handle sessions. Using this middleware saves the session id as a cookie in the client and allows us to access the session data using that id on the server. This way we keep personal account information out of the cookie used by the client to verify to our server they are authenticated and just keep the <em>key</em> to access the data stored on the server.",
|
||||||
|
"To set up Passport for use in your project, you will need to add it as a dependency first in your package.json. <code>\"passport\": \"^0.3.2\"</code>",
|
||||||
|
"In addition, add Express-session as a dependency now as well. Express-session has a ton of advanced features you can use but for now we're just going to use the basics! <code>\"express-session\": \"^1.15.0\"</code>",
|
||||||
|
"You will need to set up the session settings now and initialize Passport. Be sure to first create the variables 'session' and 'passport' to require 'express-session' and 'passport' respectively.",
|
||||||
|
"To set up your express app to use use the session we'll define just a few basic options. Be sure to add 'SESSION_SECRET' to your .env file and give it a random value. This is used to compute the hash used to encrypt your cookie!",
|
||||||
|
"<pre>app.use(session({\n secret: process.env.SESSION_SECRET,\n resave: true,\n saveUninitialized: true,\n}));</pre>",
|
||||||
|
"As well you can go ahead and tell your express app to <b>use</b> 'passport.initialize()' and 'passport.session()'. (For example, <code>app.use(passport.initialize());</code>)",
|
||||||
|
"Submit your page when you think you've got it right. If you're running into errors, you can check out the project completed up to this point <a href='https://gist.github.com/JosephLivengood/338a9c5a326923c3826a666d430e65c3'>here</a>."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Passort and Express-session are dependencies",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/package.json') .then(data => { var packJson = JSON.parse(data); assert.property(packJson.dependencies, 'passport', 'Your project should list \"passport\" as a dependency'); assert.property(packJson.dependencies, 'express-session', 'Your project should list \"express-session\" as a dependency'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Dependencies correctly required",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /require.*(\"|')passport(\"|')/gi, 'You should have required passport'); assert.match(data, /require.*(\"|')express-session(\"|')/gi, 'You should have required express-session'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Express app uses new dependencies",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /passport.initialize/gi, 'Your express app should use \"passport.initialize()\"'); assert.match(data, /passport.session/gi, 'Your express app should use \"passport.session()\"'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Session and session secret correctly set up",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /secret:( |)process.env.SESSION_SECRET/gi, 'Your express app should have express-session set up with your secret as process.env.SESSION_SECRET'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5895f70cf9fc0f352b528e66",
|
||||||
|
"title": "Serialization of a User Object",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"Serialization and deserialization are important concepts in regards to authentication. To serialize an object means to convert its contents into a small <em>key</em> essentially that can then be deserialized into the original object. This is what allows us to know whos communicated with the server without having to send the authentication data like username and password at each request for a new page.",
|
||||||
|
"To set this up properly, we need to have a serialize function and a deserialize function. In passport we create these with <code>passport.serializeUser( OURFUNCTION )</code> and <code>passport.deserializeUser( OURFUNCTION )</code>",
|
||||||
|
"The serializeUser is called with 2 arguments, the full user object and a callback used by passport. Returned in the callback should be a unique key to identify that user- the easiest one to use being the users _id in the object as it should be unique as it generated by MongoDb. Similarly deserializeUser is called with that key and a callback function for passport as well, but this time we have to take that key and return the users full object to the callback. To make a query search for a Mongo _id you will have to create <code>const ObjectID = require('mongodb').ObjectID;</code>, and then to use it you call <code>new ObjectID(THE_ID)</code>. Be sure to add MongoDB as a dependency. You can see this in the examples below:",
|
||||||
|
"<pre>passport.serializeUser((user, done) => {\n done(null, user._id);\n });</pre><br><pre>passport.deserializeUser((id, done) => {\n db.collection('users').findOne(\n {_id: new ObjectID(id)},\n (err, doc) => {\n done(null, doc);\n }\n );\n });</pre>",
|
||||||
|
"NOTE: This deserializeUser will throw an error until we set up the DB in the next step so comment out the whole block and just call <code>done(null, null)</code> in the function deserializeUser.",
|
||||||
|
"Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Serialize user function correct",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /passport.serializeUser/gi, 'You should have created your passport.serializeUser function'); assert.match(data, /null, user._id/gi, 'There should be a callback in your serializeUser with (null, user._id)'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Deserialize user function correct",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /passport.deserializeUser/gi, 'You should have created your passport.deserializeUser function'); assert.match(data, /null,( |)null/gi, 'There should be a callback in your deserializeUser with (null, null) for now'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "MongoDB is a dependency",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/package.json') .then(data => { var packJson = JSON.parse(data); assert.property(packJson.dependencies, 'mongodb', 'Your project should list \"mongodb\" as a dependency'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Mongodb properly required including the ObjectId",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /require.*(\"|')mongodb(\"|')/gi, 'You should have required mongodb'); assert.match(data, /new ObjectID.*id/gi, 'Even though the block is commented out, you should use new ObjectID(id) for when we add the database'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5895f70cf9fc0f352b528e67",
|
||||||
|
"title": "Implement the Serialization of a Passport User",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"Right now we're not loading an actually users object since we haven't set up our database. This can be done many different ways, but for our project we will connect to the database once when we start the server and keep a persistent connection for the full life-cycle of the app.",
|
||||||
|
"To do this, add MongoDB as a dependency and require it in your server. (<code>const mongo = require('mongodb').MongoClient;</code>)",
|
||||||
|
"Now we want to the connect to our database then start listening for requests. The purpose of this is to not allow requests before our database is connected or if there is a database error. To accomplish you will want to encompass your serialization and your app listener in the following:",
|
||||||
|
"<pre>mongo.connect(process.env.DATABASE, (err, db) => {\n if(err) {\n console.log('Database error: ' + err);\n } else {\n console.log('Successful database connection');\n\n //serialization and app.listen\n\n}});</pre>",
|
||||||
|
"You can now uncomment the block in deserializeUser and remove your <code>done(null, null)</code>. Be sure to set <em>DATABASE</em> in your .env file to your database's connection string (for example: <code>DATABASE=mongodb://admin:pass@mlab.com:12345/my-project</code>). You can set up a free database on <a href='https://mlab.com/welcome/'>mLab</a>. Congratulations- you've finished setting up serialization!",
|
||||||
|
"Submit your page when you think you've got it right. If you're running into errors, you can check out the project completed up to this point <a href='https://gist.github.com/JosephLivengood/e192e809a1d27cb80dc2c6d3467b7477'>here</a>."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Database connection is present",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /mongo.connect/gi, 'You should have created a connection to your database'); assert.match(data, /mongo.connect[^]*app.listen[^]*}[^]*}/gi, 'You should have your app.listen nested at within your database connection at the bottom'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Deserialization is now correctly using the DB and <code>done(null, null)</code> is erased",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.notMatch(data, /null,( |)null/gi, 'The callback in deserializeUser of (null, null) should be completely removed for the db block uncommented out'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5895f70df9fc0f352b528e68",
|
||||||
|
"title": "Authentication Strategies",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"A strategy is a way of authenticating a user. You can use a strategy for allowing users to authenticate based on locally saved information (if you have them register first) or from a variety of providers such as Google or Github. For this project we will set up a local strategy. To see a list of the 100's of strategies, visit Passports site <a href='http://passportjs.org/'>here</a>.",
|
||||||
|
"Add <em>passport-local</em> as a dependency and add it to your server as follows: <code>const LocalStrategy = require('passport-local');</code>",
|
||||||
|
"Now you will have to tell passport to <b>use</b> an instantiated LocalStartegy object with a few settings defined. Make sure this as well as everything from this point on is encapsulated in the database connection since it relies on it! <pre>passport.use(new LocalStrategy(\n function(username, password, done) {\n db.collection('users').findOne({ username: username }, function (err, user) {\n console.log('User '+ username +' attempted to log in.');\n if (err) { return done(err); }\n if (!user) { return done(null, false); }\n if (password !== user.password) { return done(null, false); }\n return done(null, user);\n });\n }\n));</pre> This is defining the process to take when we try to authenticate someone locally. First it tries to find a user in our database with the username entered, then it checks for the password to match, then finally if no errors have popped up that we checked for, like an incorrect password, the users object is returned and they are authenticated.",
|
||||||
|
"Many strategies are set up using different settings, general it is easy to set it up based on the README in that strategies repository though. A good example of this is the Github strategy where we don't need to worry about a username or password because the user will be sent to Github's auth page to authenticate and as long as they are logged in and agree then Github returns their profile for us to use.",
|
||||||
|
"In the next step we will set up how to actually call the authentication strategy to validate a user based on form data! Submit your page when you think you've got it right up to this point."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Passport-local is a dependency",
|
||||||
|
"testString": " getUserInput => $.get(getUserInput('url')+ '/_api/package.json') .then(data => { var packJson = JSON.parse(data); assert.property(packJson.dependencies, 'passport-local', 'Your project should list \"passport-local \" as a dependency'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Passport-local correctly required and setup",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /require.*(\"|')passport-local(\"|')/gi, 'You should have required passport-local'); assert.match(data, /new LocalStrategy/gi, 'You should have told passport to use a new strategy'); assert.match(data, /findOne/gi, 'Your new local strategy should use the findOne query to find a username based on the inputs'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5895f70df9fc0f352b528e69",
|
||||||
|
"title": "How to Use Passport Strategies",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"In the index.pug file supplied there is actually a login form. It has previously been hidden because of the inline javascript <code>if showLogin</code> with the form indented after it. Before showLogin as a variable was never defined, it never rendered the code block containing the form. Go ahead and on the res.render for that page add a new variable to the object <code>showLogin: true</code>. When you refresh your page, you should then see the form! This form is set up to <b>POST</b> on <em>/login</em> so this is where we should set up to accept the POST and authenticate the user.",
|
||||||
|
"For this challenge you should add the route /login to accept a POST request. To authenticate on this route you need to add a middleware to do so before then sending a response. This is done by just passing another argument with the middleware before your <code>function(req,res)</code> with your response! The middleware to use is <code>passport.authenticate('local')</code>.",
|
||||||
|
"<em>passport.authenticate</em> can also take some options as an argument such as: <code>{ failureRedirect: '/' }</code> which is incredibly useful so be sure to add that in as well. As a response after using the middleware (which will only be called if the authentication middleware passes) should be to redirect the user to <em>/profile</em> and that route should render the view 'profile.pug'.",
|
||||||
|
"If the authentication was successful, the user object will be saved in <em>req.user</em>.",
|
||||||
|
"Now at this point if you enter a username and password in the form, it should redirect to the home page <em>/</em> and in the console of your server should be 'User {USERNAME} attempted to log in.' since we currently cannot login a user who isn't registered.",
|
||||||
|
"Submit your page when you think you've got it right. If you're running into errors, you can check out the project completed up to this point <a href='https://gist.github.com/JosephLivengood/8a335d1a68ed9170da02bb9d8f5b71d5'>here</a>."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All steps correctly implemented in the server.js",
|
||||||
|
"testString": " getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /showLogin:( |)true/gi, 'You should be passing the variable \"showLogin\" as true to your render function for the homepage'); assert.match(data, /failureRedirect:( |)('|\")\\/('|\")/gi, 'Your code should include a failureRedirect to the \"/\" route'); assert.match(data, /login[^]*post[^]*local/gi, 'You should have a route for login which accepts a POST and passport.authenticates local'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "A POST request to /login correctly redirects to /",
|
||||||
|
"testString": "getUserInput => $.post(getUserInput('url')+ '/login') .then(data => { assert.match(data, /Home page/gi, 'A login attempt at this point should redirect to the homepage since we do not have any registered users'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5895f70df9fc0f352b528e6a",
|
||||||
|
"title": "Create New Middleware",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"As in, any user can just go to /profile whether they authenticated or not by typing in the url. We want to prevent this by checking if the user is authenticated first before rendering the profile page. This is the perfect example of when to create a middleware.",
|
||||||
|
"The challenge here is creating the middleware function <code>ensureAuthenticated(req, res, next)</code>, which will check if a user is authenticated by calling passports isAuthenticated on the <em>request</em> which in turn checks for <em>req.user</em> is to be defined. If it is then <em>next()</em> should be called, otherwise we can just respond to the request with a redirect to our homepage to login. An implementation of this middleware is:",
|
||||||
|
"<pre>function ensureAuthenticated(req, res, next) {\n if (req.isAuthenticated()) {\n return next();\n }\n res.redirect('/');\n};</pre>",
|
||||||
|
"Now add <em>ensureAuthenticated</em> as a middleware to the request for the profile page before the argument to the get request containing the function that renders the page.",
|
||||||
|
"<pre>app.route('/profile')\n .get(ensureAuthenticated, (req,res) => {\n res.render(process.cwd() + '/views/pug/profile');\n });</pre>",
|
||||||
|
"Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Middleware ensureAuthenticated should be implemented and on our /profile route",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /ensureAuthenticated[^]*req.isAuthenticated/gi, 'Your ensureAuthenticated middleware should be defined and utilize the req.isAuthenticated function'); assert.match(data, /profile[^]*get[^]*ensureAuthenticated/gi, 'Your ensureAuthenticated middleware should be attached to the /profile route'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "A Get request to /profile correctly redirects to / since we are not authenticated",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/profile') .then(data => { assert.match(data, /Home page/gi, 'An attempt to go to the profile at this point should redirect to the homepage since we are not logged in'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5895f70ef9fc0f352b528e6b",
|
||||||
|
"title": "How to Put a Profile Together",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"Now that we can ensure the user accessing the <em>/profile</em> is authenticated, we can use the information contained in 'req.user' on our page!",
|
||||||
|
"Go ahead and pass the object containing the variable <em>username</em> equaling 'req.user.username' into the render method of the profile view. Then go to your 'profile.pug' view and add the line <code>h2.center#welcome Welcome, #{username}!</code> creating the h2 element with the class 'center' and id 'welcome' containing the text 'Welcome, ' and the username!",
|
||||||
|
"Also in the profile, add a link to <em>/logout</em>. That route will host the logic to unauthenticate a user. <code>a(href='/logout') Logout</code>",
|
||||||
|
"Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Correctly added a Pug render variable to /profile",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /\\/views\\/pug\\/profile[^]*username:( |)req.user.username/gi, 'You should be passing the variable username with req.user.username into the render function of the profile page'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "58965611f9fc0f352b528e6c",
|
||||||
|
"title": "Logging a User Out",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"Creating the logout logic is easy. The route should just unauthenticate the user and redirect to the home page instead of rendering any view.",
|
||||||
|
"In passport, unauthenticating a user is as easy as just calling <code>req.logout();</code> before redirecting.",
|
||||||
|
"<pre>app.route('/logout')\n .get((req, res) => {\n req.logout();\n res.redirect('/');\n });</pre>",
|
||||||
|
"You may have noticed we also we're not handling missing pages (404), the common way to handle this in Node is with the following middleware. Go ahead and add this in after all your other routes:",
|
||||||
|
"<pre>app.use((req, res, next) => {\n res.status(404)\n .type('text')\n .send('Not Found');\n});</pre>",
|
||||||
|
"Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Logout route",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /req.logout/gi, 'You should be call req.logout() in youre /logout route'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Logout should redirect to the home page /",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/logout') .then(data => { assert.match(data, /Home page/gi, 'When a user logs out they should be redirected to the homepage'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "58966a17f9fc0f352b528e6d",
|
||||||
|
"title": "Registration of New Users",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"Now we need to allow a new user on our site to register an account. On the res.render for the home page add a new variable to the object passed along- <code>showRegistration: true</code>. When you refresh your page, you should then see the registration form that was already created in your index.pug file! This form is set up to <b>POST</b> on <em>/register</em> so this is where we should set up to accept the POST and create the user object in the database.",
|
||||||
|
"The logic of the registration route should be as follows: Register the new user > Authenticate the new user > Redirect to /profile",
|
||||||
|
"The logic of step 1, registering the new user, should be as follows: Query database with a findOne command > if user is returned then it exists and redirect back to home <em>OR</em> if user is undefined and no error occurs then 'insertOne' into the database with the username and password and as long as no errors occur then call <em>next</em> to go to step 2, authenticating the new user, which we've already written the logic for in our POST /login route.",
|
||||||
|
"<pre>app.route('/register')\n .post((req, res, next) => {\n db.collection('users').findOne({ username: req.body.username }, function (err, user) {\n if(err) {\n next(err);\n } else if (user) {\n res.redirect('/');\n } else {\n db.collection('users').insertOne(\n {username: req.body.username,\n password: req.body.password},\n (err, doc) => {\n if(err) {\n res.redirect('/');\n } else {\n next(null, user);\n }\n }\n )\n }\n })},\n passport.authenticate('local', { failureRedirect: '/' }),\n (req, res, next) => {\n res.redirect('/profile');\n }\n);</pre>",
|
||||||
|
"Submit your page when you think you've got it right. If you're running into errors, you can check out the project completed up to this point <a href='https://gist.github.com/JosephLivengood/6c47bee7df34df9f11820803608071ed'>here</a>."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Register route and display on home",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /showRegistration:( |)true/gi, 'You should be passing the variable \"showRegistration\" as true to your render function for the homepage'); assert.match(data, /register[^]*post[^]*findOne[^]*username:( |)req.body.username/gi, 'You should have a route accepted a post request on register that querys the db with findone and the query being \"username: req.body.username\"'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Registering should work",
|
||||||
|
"testString": "getUserInput => $.ajax({url: getUserInput('url')+ '/register',data: {username: 'freeCodeCampTester', password: 'freeCodeCampTester'},crossDomain: true, type: 'POST', xhrFields: { withCredentials: true }}) .then(data => { assert.match(data, /Profile/gi, 'I should be able to register and it direct me to my profile. CLEAR YOUR DATABASE if this test fails (each time until its right!)'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Login should work",
|
||||||
|
"testString": "getUserInput => $.ajax({url: getUserInput('url')+ '/login',data: {username: 'freeCodeCampTester', password: 'freeCodeCampTester'}, type: 'POST', xhrFields: { withCredentials: true }}) .then(data => { assert.match(data, /Profile/gi, 'Login should work if previous test was done successfully and redirect successfully to the profile. Check your work and clear your DB'); assert.match(data, /freeCodeCampTester/gi, 'The profile should properly display the welcome to the user logged in'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Logout should work",
|
||||||
|
"testString": "getUserInput => $.ajax({url: getUserInput('url')+ '/logout', type: 'GET', xhrFields: { withCredentials: true }}) .then(data => { assert.match(data, /Home/gi, 'Logout should redirect to home'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Profile should no longer work after logout",
|
||||||
|
"testString": "getUserInput => $.ajax({url: getUserInput('url')+ '/profile', type: 'GET', crossDomain: true, xhrFields: { withCredentials: true }}) .then(data => { assert.match(data, /Home/gi, 'Profile should redirect to home when we are logged out now again'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "58a25c98f9fc0f352b528e7f",
|
||||||
|
"title": "Hashing Your Passwords",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"Going back to the information security section you may remember that storing plaintext passwords is <em>never</em> okay. Now it is time to implement BCrypt to solve this issue.",
|
||||||
|
"<hr>Add BCrypt as a dependency and require it in your server. You will need to handle hashing in 2 key areas: where you handle registering/saving a new account and when you check to see that a password is correct on login.",
|
||||||
|
"Currently on our registeration route, you insert a user's password into the database like the following: <code>password: req.body.password</code>. An easy way to implement saving a hash instead is to add the following before your database logic <code>var hash = bcrypt.hash(req.body.password, 12);</code> and replacing the <code>req.body.password</code> in the database saving with just <code>password: hash</code>.",
|
||||||
|
"Finally on our authentication strategy we check for the following in our code before completing the process: <code>if (password !== user.password) { return done(null, false); }</code>. After making the previous changes, now <code>user.password</code> is a hash. Before making a change to the existing code, notice how the statement is checking if the password is NOT equal then return non-authenticated. With this in mind your code could look as follows to properly check the password entered against the hash: <code>if (!bcrypt.compare(password, user.password)) { return done(null, false); }</code>",
|
||||||
|
"That is all it takes to implement one of the most important security features when you have to store passwords! Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "BCrypt is a dependency",
|
||||||
|
"testString": " getUserInput => $.get(getUserInput('url')+ '/_api/package.json') .then(data => { var packJson = JSON.parse(data); assert.property(packJson.dependencies, 'bcrypt', 'Your project should list \"bcrypt\" as a dependency'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "BCrypt correctly required and implemented",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /require.*(\"|')bcrypt(\"|')/gi, 'You should have required bcrypt'); assert.match(data, /bcrypt.hash/gi, 'You should use hash the password in the registration'); assert.match(data, /bcrypt.compare/gi, 'You should compare the password to the hash in your strategy'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "589690e6f9fc0f352b528e6e",
|
||||||
|
"title": "Clean Up Your Project with Modules",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-advancednode/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-advancednode/'>GitHub</a>.",
|
||||||
|
"Right now everything you have is in your server.js file. This can lead to hard to manage code that isn't very expandable.",
|
||||||
|
"Create 2 new files: Routes.js and Auth.js",
|
||||||
|
"Both should start with the following code: <pre>module.exports = function (app, db) {\n\n\n}</pre>",
|
||||||
|
"Now in the top of your server file, require these files like such: <code>const routes = require('./routes.js');</code>",
|
||||||
|
"Right after you establish a successful connect with the database instantiate each of them like such: <code>routes(app, db)</code>",
|
||||||
|
"Finally, take all of the routes in your server and paste them into your new files and remove them from your server file. Also take the ensureAuthenticated since we created that middleware function for routing specifically. You will have to now correctly add the dependencies in that are used, such as <code>const passport = require('passport');</code>, at the very top above the export line in your routes.js file.",
|
||||||
|
"Keep adding them until no more errors exist, and your server file no longer has any routing!",
|
||||||
|
"Now do the same thing in your auth.js file with all of the things related to authentication such as the serialization and the setting up of the local strategy and erase them from your server file. Be sure to add the dependencies in and call <code>auth(app,db)</code> in the server in the same spot. Be sure to have <code>auth(app, db)</code> before <code>routes(app, db)</code> since our registration route depends on passport being initiated!",
|
||||||
|
"Congratulations- you're at the end of this section of Advanced Node and Express and have some beautiful code to show for it! Submit your page when you think you've got it right. If you're running into errors, you can check out an example of the completed project <a href='https://glitch.com/#!/project/delicious-herring'>here</a>."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Modules present",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /require.*(\"|').\\/routes.js(\"|')/gi, 'You should have required your new files'); assert.match(data, /mongo.connect[^]*routes/gi, 'Your new modules should be called after your connection to the database'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "589a69f5f9fc0f352b528e70",
|
||||||
|
"title": "Implementation of Social Authentication",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-socialauth/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-socialauth/'>GitHub</a>.",
|
||||||
|
"The basic path this kind of authentication will follow in your app is: <ol><li>User clicks a button or link sending them to our route to authenticate using a specific strategy (EG. Github)</li><li>Your route calls <code>passport.authenticate('github')</code> which redirects them to Github.</li><li>The page the user lands on, on Github, allows them to login if they aren't already then asks them to approve access to their profile from our app.</li><li>The user is then returned to our app at a specific callback url with their profile if they approved.</li><li>They are now authenticated and your app should check if it is a returning profile, or save it in your database if it is not.</li></ol>",
|
||||||
|
"Strategies with OAuth require you to have at least a <em>Client ID</em> and a <em>Client Secret</em> which is a way for them to verify who the authentication request is coming from and if it is valid. These are obtained from the site you are trying to implement authentication with, such as Github, and are unique to your app- <b>THEY ARE NOT TO BE SHARED</b> and should never be uploaded to a public repository or written directly in your code. A common practice is to put them in your <em>.env</em> file and reference them like: <code>process.env.GITHUB_CLIENT_ID</code>. For this challenge we're going to use the Github strategy.",
|
||||||
|
"Obtaining your <em>Client ID and Secret<em> from Github is done in your account profile settings under 'developer settings', then '<a href='https://github.com/settings/developers'>OAuth applications</a>'. Click 'Register a new application', name your app, paste in the url to your glitch homepage (<b>Not the project code's url</b>), and lastly for the callback url, paste in the same url as the homepage but with '/auth/github/callback' added on. This is where users will be redirected to for us to handle after authenticating on Github. Save the returned information as 'GITHUB_CLIENT_ID' and 'GITHUB_CLIENT_SECRET' in your .env file.",
|
||||||
|
"On your remixed project, create 2 routes accepting GET requests: /auth/github and /auth/github/callback. The first should only call passport to authenticate 'github' and the second should call passport to authenticate 'github' with a failure redirect to '/' and then if that is successful redirect to '/profile' (similar to our last project).",
|
||||||
|
"An example of how '/auth/github/callback' should look is similar to how we handled a normal login in our last project: <pre>app.route('/login')\n .post(passport.authenticate('local', { failureRedirect: '/' }), (req,res) => { \n res.redirect('/profile'); \n });</pre>",
|
||||||
|
"Submit your page when you think you've got it right. If you're running into errors, you can check out the project up to this point <a href='https://gist.github.com/JosephLivengood/28ea2cae7e1dc6a53d7f0c42d987313b'>here</a>."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Route /auth/github correct",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /('|\")\\/auth\\/github('|\")[^]*get.*passport.authenticate.*github/gi, 'Route auth/github should only call passport.authenticate with github'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Route /auth/github/callback correct",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /('|\")\\/auth\\/github\\/callback('|\")[^]*get.*passport.authenticate.*github.*failureRedirect:( |)(\"|')\\/(\"|')/gi, 'Route auth/github/callback should accept a get request and call passport.authenticate for github with a failure redirect to home'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "589a69f5f9fc0f352b528e71",
|
||||||
|
"title": "Implementation of Social Authentication II",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-socialauth/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-socialauth/'>GitHub</a>.",
|
||||||
|
"The last part of setting up your Github authentication is to create the strategy itself. For this, you will need to add the dependency of 'passport-github' to your project and require it as GithubStrategy like <code>const GitHubStrategy = require('passport-github').Strategy;</code>.",
|
||||||
|
"To set up the Github strategy, you have to tell <b>passport</b> to <b>use</b> an instantiated <b>GithubStrategy</b>, which accepts 2 arguments: An object (containing <em>clientID</em>, <em>clientSecret</em>, and <em>callbackURL</em>) and a function to be called when a user is successfully authenticated which we will determine if the user is new and what fields to save initially in the user's database object. This is common across many strategies but some may require more information as outlined in that specific strategy's github README; for example, Google requires a <em>scope</em> as well which determines what kind of information your request is asking returned and asks the user to approve such access. The current strategy we are implementing has its usage outlined <a>here</a>, but we're going through it all right here on freeCodeCamp!",
|
||||||
|
"Here's how your new strategy should look at this point: <pre>passport.use(new GitHubStrategy({\n clientID: process.env.GITHUB_CLIENT_ID,\n clientSecret: process.env.GITHUB_CLIENT_SECRET,\n callbackURL: /*INSERT CALLBACK URL ENTERED INTO GITHUB HERE*/\n },\n function(accessToken, refreshToken, profile, cb) {\n console.log(profile);\n //Database logic here with callback containing our user object\n }\n));</pre>",
|
||||||
|
"Your authentication won't be successful yet, and actually throw an error, without the database logic and callback, but it should log to your console your Github profile if you try it!",
|
||||||
|
"Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Dependency added",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/package.json') .then(data => { var packJson = JSON.parse(data); assert.property(packJson.dependencies, 'passport-github', 'Your project should list \"passport-github\" as a dependency'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Dependency required",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /require.*(\"|')passport-github(\"|')/gi, 'You should have required passport-github'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Github strategy setup correctly thus far",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /passport.use.*new GitHubStrategy/gi, 'Passport should use a new GitHubStrategy'); assert.match(data, /callbackURL:( |)(\"|').*(\"|')/gi, 'You should have a callbackURL'); assert.match(data, /process.env.GITHUB_CLIENT_SECRET/g, 'You should use process.env.GITHUB_CLIENT_SECRET'); assert.match(data, /process.env.GITHUB_CLIENT_ID/g, 'You should use process.env.GITHUB_CLIENT_ID'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "589a8eb3f9fc0f352b528e72",
|
||||||
|
"title": "Implementation of Social Authentication III",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-socialauth/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-socialauth/'>GitHub</a>.",
|
||||||
|
"The final part of the strategy is handling the profile returned from Github. We need to load the users database object if it exists or create one if it doesn't and populate the fields from the profile, then return the user's object. Github supplies us a unique <em>id</em> within each profile which we can use to search with to serialize the user with (already implemented). Below is an example implementation you can use in your project- it goes within the function that is the second argument for the new strategy, right below the <code>console.log(profile);</code> currently is:",
|
||||||
|
"<pre>db.collection('socialusers').findAndModify(\n {id: profile.id},\n {},\n {$setOnInsert:{\n id: profile.id,\n name: profile.displayName || 'John Doe',\n photo: profile.photos[0].value || '',\n email: profile.emails[0].value || 'No public email',\n created_on: new Date(),\n provider: profile.provider || ''\n },$set:{\n last_login: new Date()\n },$inc:{\n login_count: 1\n }},\n {upsert:true, new: true},\n (err, doc) => {\n return cb(null, doc.value);\n }\n);</pre>",
|
||||||
|
"With a findAndModify, it allows you to search for an object and update it, as well as upsert the object if it doesn't exist and receive the new object back each time in our callback function. In this example, we always set the last_login as now, we always increment the login_count by 1, and only when we insert a new object(new user) do we populate the majority of the fields. Something to notice also is the use of default values. Sometimes a profile returned won't have all the information filled out or it will have been chosen by the user to remain private; so in this case we have to handle it to prevent an error.",
|
||||||
|
"You should be able to login to your app now- try it! Submit your page when you think you've got it right. If you're running into errors, you can check out an example of this mini-project's finished code <a href='https://glitch.com/#!/project/guttural-birch'>here</a>."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Github strategy setup complete",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /GitHubStrategy[^]*db.collection/gi, 'Strategy should use now use the database to search for the user'); assert.match(data, /GitHubStrategy[^]*socialusers/gi, 'Strategy should use \"socialusers\" as db collection'); assert.match(data, /GitHubStrategy[^]*return cb/gi, 'Strategy should return the callback function \"cb\"'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "589fc830f9fc0f352b528e74",
|
||||||
|
"title": "Set up the Enviroment",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-socketio/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-socketio/'>GitHub</a>.",
|
||||||
|
"Add Socket.IO as a dependency and require/instanciate it in your server defined as 'io' with the http server as an argument. <code>const io = require('socket.io')(http);</code>",
|
||||||
|
"The first thing needing to be handled is listening for a new connection from the client. The <dfn>on</dfn> keyword does just that- listen for a specific event. It requires 2 arguments: a string containing the title of the event thats emitted, and a function with which the data is passed though. In the case of our connection listener, we use <em>socket</em> to define the data in the second argument. A socket is an individual client who is connected.",
|
||||||
|
"For listening for connections on our server, add the following between the comments in your project:<pre>io.on('connection', socket => {\n console.log('A user has connected');\n});</pre>",
|
||||||
|
"Now for the client to connect, you just need to add the following to your client.js which is loaded by the page after you've authenticated: <pre>/*global io*/\nvar socket = io();</pre>The comment supresses the error you would normally see since 'io' is not defined in the file. We've already added a reliable CDN to the Socket.IO library on the page in chat.pug.",
|
||||||
|
"Now try loading up your app and authenticate and you should see in your server console 'A user has connected'!",
|
||||||
|
"<strong>Note</strong><br><code>io()</code> works only when connecting to a socket hosted on the same url/server. For connecting to an external socket hosted elsewhere, you would use <code>io.connect('URL');</code>.",
|
||||||
|
"Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Socket.IO is a dependency",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/package.json') .then(data => { var packJson = JSON.parse(data); assert.property(packJson.dependencies, 'socket.io', 'Your project should list \"socket.io\" as a dependency'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Socket.IO has been properly required and instanciated",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js').then(data => {assert.match(data, /io.*=.*require.*('|\")socket.io('|\").*http/gi, 'You should correctly require and instanciate socket.io as io.');}, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Socket.IO should be listening for connections",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /io.on.*('|\")connection('|\").*socket/gi, 'io should listen for \"connection\" and socket should be the 2nd arguments variable'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Your client should connect to your server",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.*=.*io/gi, 'Your client should be connection to server with the connection defined as socket'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "589fc831f9fc0f352b528e75",
|
||||||
|
"title": "Communicate by Emitting",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-socketio/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-socketio/'>GitHub</a>.",
|
||||||
|
"<dfn>Emit</dfn> is the most common way of communicating you will use. When you emit something from the server to 'io', you send an event's name and data to all the connected sockets. A good example of this concept would be emiting the current count of connected users each time a new user connects!",
|
||||||
|
"<hr>Start by adding a variable to keep track of the users just before where you are currently listening for connections. <code>var currentUsers = 0;</code>",
|
||||||
|
"Now when someone connects you should increment the count before emiting the count so you will want to add the incrementer within the connection listener. <code>++currentUsers;</code>",
|
||||||
|
"Finally after incrementing the count, you should emit the event(still within the connection listener). The event should be named 'user count' and the data should just be the 'currentUsers'. <code>io.emit('user count', currentUsers);</code>",
|
||||||
|
"<hr>Now you can implement a way for your client to listen for this event! Similarly to listening for a connection on the server you will use the <em>on</em> keyword. <pre>socket.on('user count', function(data){\n console.log(data);\n});</pre>",
|
||||||
|
"Now try loading up your app and authenticate and you should see in your client console '1' representing the current user count! Try loading more clients up and authenticating to see the number go up.",
|
||||||
|
"Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "currentUsers is defined",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js').then(data => {assert.match(data, /currentUsers/gi, 'You should have variable currentUsers defined');}, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Server emits the current user count at each new connection",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /io.emit.*('|\")user count('|\").*currentUsers/gi, 'You should emit \"user count\" with data currentUsers'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Your client is listening for 'user count' event",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.on.*('|\")user count('|\")/gi, 'Your client should be connection to server with the connection defined as socket'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "589fc831f9fc0f352b528e76",
|
||||||
|
"title": "Handle a Disconnect",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-socketio/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-socketio/'>GitHub</a>.",
|
||||||
|
"You may notice that up to now you have only been increasing the user count. Handling a user disconnecting is just as easy as handling the initial connect except the difference is you have to listen for it on each socket versus on the whole server.",
|
||||||
|
"<hr>To do this, add in to your existing connect listener a listener that listens for 'disconnect' on the socket with no data passed through. You can test this functionality by just logging to the console a user has disconnected. <code>socket.on('disconnect', () => { /*anything you want to do on disconnect*/ });</code>",
|
||||||
|
"To make sure clients continuously have the updated count of current users, you should decrease the currentUsers by 1 when the disconnect happens then emit the 'user count' event with the updated count!",
|
||||||
|
"<strong>Note</strong><br>Just like 'disconnect', all other events that a socket can emit to the server should be handled within the connecting listener where we have 'socket' defined.",
|
||||||
|
"Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Server handles the event disconnect from a socket",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /socket.on.*('|\")disconnect('|\")/gi, ''); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Your client is listening for 'user count' event",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.on.*('|\")user count('|\")/gi, 'Your client should be connection to server with the connection defined as socket'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "589fc831f9fc0f352b528e77",
|
||||||
|
"title": "Authentication with Socket.IO",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-socketio/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-socketio/'>GitHub</a>.",
|
||||||
|
"Currently, you cannot determine who is connected to your web socket. While 'req.user' containers the user object, thats only when your user interacts with the web server and with web sockets you have no req (request) and therefor no user data. One way to solve the problem of knowing who is connected to your web socket is by parsing and decoding the cookie that contains the passport session then deserializing it to obtain the user object. Luckily, there is a package on NPM just for this that turns a once complex task into something simple!",
|
||||||
|
"<hr>Add 'passport.socketio' as a dependency and require it as 'passportSocketIo'.",
|
||||||
|
"Now we just have to tell Socket.IO to use it and set the options. Be sure this is added before the existing socket code and not in the existing connection listener. For your server it should look as follows:<pre>io.use(passportSocketIo.authorize({\n cookieParser: cookieParser,\n key: 'express.sid',\n secret: process.env.SESSION_SECRET,\n store: sessionStore\n}));</pre>You can also optionally pass 'success' and 'fail' with a function that will be called after the authentication process completes when a client trys to connect.",
|
||||||
|
"The user object is now accessable on your socket object as <code>socket.request.user</code>. For example, now you can add the following: <code>console.log('user ' + socket.request.user.name + ' connected');</code> and it will log to the server console who has connected!",
|
||||||
|
"Submit your page when you think you've got it right. If you're running into errors, you can check out the project up to this point <a href='https://gist.github.com/JosephLivengood/a9e69ff91337500d5171e29324e1ff35'>here</a>."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "passportSocketIo is a dependency",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/package.json') .then(data => { var packJson = JSON.parse(data); assert.property(packJson.dependencies, 'passportSocketIo', 'Your project should list \"passportSocketIo\" as a dependency'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "passportSocketIo is properly required",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js').then(data => {assert.match(data, /passportSockerIo.*=.*require.*('|\")passportSocketIo('|\")/gi, 'You should correctly require and instanciate socket.io as io.');}, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "passportSocketIo is properly setup",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /io.use.*passportSocketIo.authorize/gi, 'You should have told io to use passportSockIo.authorize with the correct options'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "589fc832f9fc0f352b528e78",
|
||||||
|
"title": "Announce New Users",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-socketio/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-socketio/'>GitHub</a>.",
|
||||||
|
"Many chat rooms are able to annouce when a user connects or disconnects and then display that to all of the connected users in the chat. Seeing as though you already are emitting an event on connect and disconnect, you will just have to modify this event to support such feature. The most logical way of doing so is sending 3 pieces of data with the event: name of the user connected/disconnected, the current user count, and if that name connected or disconnected.",
|
||||||
|
"<hr>Change the event name to 'user' and as the data pass an object along containing fields 'name', 'currentUsers', and boolean 'connected' (to be true if connection, or false for disconnection of the user sent). Be sure to make the change to both points we had the 'user count' event and set the disconnect one to sent false for field 'connected' instead of true like the event emitted on connect. <code>io.emit('user', {name: socket.request.user.name, currentUsers, connected: true});</code>",
|
||||||
|
"Now your client will have all the nesesary information to correctly display the current user count and annouce when a user connects or disconnects! To handle this event on the client side we should listen for 'user' and then update the current user count by using jQuery to change the text of <code>#num-users</code> to '{NUMBER} users online', as well as append a <code><li></code> to the unordered list with id 'messages' with '{NAME} has {joined/left} the chat.'.",
|
||||||
|
"An implementation of this could look like the following:<pre>socket.on('user', function(data){\n $('#num-users').text(data.currentUsers+' users online');\n var message = data.name;\n if(data.connected) {\n message += ' has joined the chat.';\n } else {\n message += ' has left the chat.';\n }\n $('#messages').append($('<li>').html('<b>'+ message +'<\\/b>'));\n});</pre>",
|
||||||
|
"Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Event 'user' is emitted with name, currentUsers, and connected",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /io.emit.*('|\")user('|\").*name.*currentUsers.*connected/gi, 'You should have an event emitted named user sending name, currentUsers, and connected'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Client properly handling and displaying the new data from event 'user'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.on.*('|\")user('|\")[^]*num-users/gi, 'You should change the text of #num-users within on your client within the \"user\" even listener to show the current users connected'); assert.match(data, /socket.on.*('|\")user('|\")[^]*messages.*li/gi, 'You should append a list item to #messages on your client within the \"user\" event listener to annouce a user came or went'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "589fc832f9fc0f352b528e79",
|
||||||
|
"title": "Send and Display Chat Messages",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-socketio/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-socketio/'>GitHub</a>.",
|
||||||
|
"It's time you start allowing clients to send a chat message to the server to emit to all the clients! Already in your client.js file you should see there is already a block of code handling when the messgae form is submitted! (<code>$('form').submit(function(){ /*logic*/ });</code>)",
|
||||||
|
"<hr>Within the code you're handling the form submit you should emit an event after you define 'messageToSend' but before you clear the text box <code>#m</code>. The event should be named 'chat message' and the data should just be 'messageToSend'. <code>socket.emit('chat message', messageToSend);</code>",
|
||||||
|
"Now on your server you should be listening to the socket for the event 'chat message' with the data being named 'message'. Once the event is recieved it should then emit the event 'chat message' to all sockets <code>io.emit</code> with the data being an object containing 'name' and 'message'.",
|
||||||
|
"On your client now again, you should now listen for event 'chat message' and when recieved, append a list item to <code>#messages</code> with the name a colon and the message!",
|
||||||
|
"At this point the chat should be fully functional and sending messages across all clients! Submit your page when you think you've got it right. If you're running into errors, you can check out the project up to this point <a href='https://gist.github.com/JosephLivengood/3e4b7750f6cd42feaa2768458d682136'>here for the server</a> and <a href='https://gist.github.com/JosephLivengood/41ba76348df3013b7870dc64861de744'>here for the client</a>."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Server listens for 'chat message' then emits it properly",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /socket.on.*('|\")chat message('|\")[^]*io.emit.*('|\")chat message('|\").*name.*message/gi, 'Your server should listen to the socket for \"chat message\" then emit to all users \"chat message\" with name and message in the data object'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Client properly handling and displaying the new data from event 'chat message'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.on.*('|\")chat message('|\")[^]*messages.*li/gi, 'You should append a list item to #messages on your client within the \"chat message\" event listener to display the new message'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"translations": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,337 @@
|
|||||||
|
{
|
||||||
|
"name": "Information Security with HelmetJS",
|
||||||
|
"order": 1,
|
||||||
|
"time": "5 hours",
|
||||||
|
"helpRoom": "HelpBackend",
|
||||||
|
"challenges": [
|
||||||
|
{
|
||||||
|
"id": "587d8247367417b2b2512c36",
|
||||||
|
"title": "Install and Require Helmet",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-infosec/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-infosec/'>GitHub</a>.",
|
||||||
|
"Helmet helps you secure your Express apps by setting various HTTP headers. Install the package, then require it."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "\"helmet\" dependency should be in package.json",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/package.json').then(data => { var packJson = JSON.parse(data); assert.property(packJson.dependencies, 'helmet'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8247367417b2b2512c37",
|
||||||
|
"title": "Hide Potentially Dangerous Information Using helmet.hidePoweredBy()",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-infosec/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-infosec/'>GitHub</a>.",
|
||||||
|
"Hackers can exploit known vulnerabilities in Express/Node if they see that your site is powered by Express. X-Powered-By: Express is sent in every request coming from Express by default. The helmet.hidePoweredBy() middleware will remove the X-Powered-By header. You can also explicitly set the header to something else, to throw people off. e.g. app.use(helmet.hidePoweredBy({ setTo: 'PHP 4.2.0' }))"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "helmet.hidePoweredBy() middleware should be mounted correctly",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/app-info').then(data => { assert.include(data.appStack, 'hidePoweredBy'); assert.notEqual(data.headers['x-powered-by'], 'Express')}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8247367417b2b2512c38",
|
||||||
|
"title": "Mitigate the Risk of Clickjacking with helmet.frameguard()",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-infosec/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-infosec/'>GitHub</a>.",
|
||||||
|
"Your page could be put in a <frame> or <iframe> without your consent. This can result in clickjacking attacks, among other things. Clickjacking is a technique of tricking a user into interacting with a page different from what the user thinks it is. This can be obtained executing your page in a malicious context, by mean of iframing. In that context a hacker can put a hidden layer over your page. Hidden buttons can be used to run bad scripts. This middleware sets the X-Frame-Options header. It restricts who can put your site in a frame. It has three modes: DENY, SAMEORIGIN, and ALLOW-FROM.",
|
||||||
|
"We don’t need our app to be framed. You should use helmet.frameguard() passing with the configuration object {action: 'deny'}."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "helmet.frameguard() middleware should be mounted correctly",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/app-info').then(data => { assert.include(data.appStack, 'frameguard', 'helmet.frameguard() middleware is not mounted correctly'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "helmet.frameguard() 'action' should be set to 'DENY'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/app-info').then(data => { assert.property(data.headers, 'x-frame-options'); assert.equal(data.headers['x-frame-options'], 'DENY');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8247367417b2b2512c39",
|
||||||
|
"title": "Mitigate the Risk of Cross Site Scripting (XSS) Attacks with helmet.xssFilter()",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-infosec/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-infosec/'>GitHub</a>.",
|
||||||
|
"Cross-site scripting (XSS) is a frequent type of attack where malicious scripts are injected into vulnerable pages, with the purpose of stealing sensitive data like session cookies, or passwords.",
|
||||||
|
"The basic rule to lower the risk of an XSS attack is simple: “Never trust user’s input”. As a developer you should always sanitize all the input coming from the outside. This includes data coming from forms, GET query urls, and even from POST bodies. Sanitizing means that you should find and encode the characters that may be dangerous e.g. <, >.",
|
||||||
|
"Modern browsers can help mitigating the risk by adopting better software strategies. Often these are configurable via http headers.",
|
||||||
|
"The X-XSS-Protection HTTP header is a basic protection. The browser detects a potential injected script using a heuristic filter. If the header is enabled, the browser changes the script code, neutralizing it.",
|
||||||
|
"It still has limited support."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "helmet.xssFilter() middleware should be mounted correctly",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/app-info').then(data => { assert.include(data.appStack, 'xXssProtection'); assert.property(data.headers, 'x-xss-protection'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8248367417b2b2512c3a",
|
||||||
|
"title": "Avoid Inferring the Response MIME Type with helmet.noSniff()",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-infosec/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-infosec/'>GitHub</a>.",
|
||||||
|
"Browsers can use content or MIME sniffing to adapt to different datatypes coming from a response. They override the Content-Type headers to guess and process the data. While this can be convenient in some scenarios, it can also lead to some dangerous attacks. This middleware sets the X-Content-Type-Options header to nosniff. This instructs the browser to not bypass the provided Content-Type."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "helmet.noSniff() middleware should be mounted correctly",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/app-info').then(data => { assert.include(data.appStack, 'nosniff'); assert.equal(data.headers['x-content-type-options'], 'nosniff'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8248367417b2b2512c3b",
|
||||||
|
"title": "Prevent IE from Opening Untrusted HTML with helmet.ieNoOpen()",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-infosec/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-infosec/'>GitHub</a>.",
|
||||||
|
"Some web applications will serve untrusted HTML for download. Some versions of Internet Explorer by default open those HTML files in the context of your site. This means that an untrusted HTML page could start doing bad things in the context of your pages. This middleware sets the X-Download-Options header to noopen. This will prevent IE users from executing downloads in the trusted site’s context."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "helmet.ieNoOpen() middleware should be mounted correctly",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/app-info').then(data => { assert.include(data.appStack, 'ienoopen'); assert.equal(data.headers['x-download-options'], 'noopen'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8248367417b2b2512c3c",
|
||||||
|
"title": "Ask Browsers to Access Your Site via HTTPS Only with helmet.hsts()",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-infosec/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-infosec/'>GitHub</a>.",
|
||||||
|
"HTTP Strict Transport Security (HSTS) is a web security policy which helps to protect websites against protocol downgrade attacks and cookie hijacking. If your website can be accessed via HTTPS you can ask user’s browsers to avoid using insecure HTTP. By setting the header Strict-Transport-Security, you tell the browsers to use HTTPS for the future requests in a specified amount of time. This will work for the requests coming after the initial request.",
|
||||||
|
"Configure helmet.hsts() to use HTTPS for the next 90 days. Pass the config object {maxAge: timeInMilliseconds, force: true}. Glitch already has hsts enabled. To override its settings you need to set the field \"force\" to true in the config object. We will intercept and restore the Glitch header, after inspecting it for testing.",
|
||||||
|
"Note: Configuring HTTPS on a custom website requires the acquisition of a domain, and a SSL/TSL Certificate."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "helmet.hsts() middleware should be mounted correctly",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/app-info').then(data => { assert.include(data.appStack, 'hsts'); assert.property(data.headers, 'strict-transport-security'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "maxAge should be equal to 7776000 ms (90 days)",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/app-info').then(data => { assert.match(data.headers['strict-transport-security'], /^max-age=777600000;?/); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8248367417b2b2512c3d",
|
||||||
|
"title": "Disable DNS Prefetching with helmet.dnsPrefetchControl()",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-infosec/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-infosec/'>GitHub</a>.",
|
||||||
|
"To improve performance, most browsers prefetch DNS records for the links in a page. In that way the destination ip is already known when the user clicks on a link. This may lead to over-use of the DNS service (if you own a big website, visited by millions people…), privacy issues (one eavesdropper could infer that you are on a certain page), or page statistics alteration (some links may appear visited even if they are not). If you have high security needs you can disable DNS prefetching, at the cost of a performance penalty."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "helmet.dnsPrefetchControl() middleware should be mounted correctly",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/app-info').then(data => { assert.include(data.appStack, 'dnsPrefetchControl'); assert.equal(data.headers['x-dns-prefetch-control'], 'off'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8249367417b2b2512c3e",
|
||||||
|
"title": "Disable Client-Side Caching with helmet.noCache()",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-infosec/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-infosec/'>GitHub</a>.",
|
||||||
|
"If you are releasing an update for your website, and you want the users to always download the newer version, you can (try to) disable caching on client’s browser. It can be useful in development too. Caching has performance benefits, which you will lose, so only use this option when there is a real need."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "helmet.noCache() middleware should be mounted correctly",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/app-info').then(data => { assert.include(data.appStack, 'nocache'); assert.equal(data.headers['cache-control'], 'no-store, no-cache, must-revalidate, proxy-revalidate'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8249367417b2b2512c3f",
|
||||||
|
"title": "Set a Content Security Policy with helmet.contentSecurityPolicy()",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-infosec/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-infosec/'>GitHub</a>.",
|
||||||
|
"This challenge highlights one promising new defense that can significantly reduce the risk and impact of many type of attacks in modern browsers. By setting and configuring a Content Security Policy you can prevent the injection of anything unintended into your page. This will protect your app from XSS vulnerabilities, undesired tracking, malicious frames, and much more. CSP works by defining a whitelist of content sources which are trusted. You can configure them for each kind of resource a web page may need (scripts, stylesheets, fonts, frames, media, and so on…). There are multiple directives available, so a website owner can have a granular control. See HTML 5 Rocks, KeyCDN for more details. Unfortunately CSP in unsupported by older browser.",
|
||||||
|
"By default, directives are wide open, so it’s important to set the defaultSrc directive as a fallback. Helmet supports both defaultSrc and default-src naming styles. The fallback applies for most of the unspecified directives. In this exercise, use helmet.contentSecurityPolicy(), and configure it setting the defaultSrc directive to [\"self\"] (the list of allowed sources must be in an array), in order to trust only your website address by default. Set also the scriptSrc directive so that you will allow scripts to be downloaded from your website, and from the domain 'trusted-cdn.com'.",
|
||||||
|
"Hint: in the \"'self'\" keyword, the single quotes are part of the keyword itself, so it needs to be enclosed in double quotes to be working."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "helmet.csp() middleware should be mounted correctly",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/app-info').then(data => { assert.include(data.appStack, 'csp'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Your csp config is not correct. defaultSrc should be [\"'self'\"] and scriptSrc should be [\"'self'\", 'trusted-cdn.com']",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/app-info').then(data => { var cspHeader = Object.keys(data.headers).filter(function(k){ return k === 'content-security-policy' || k === 'x-webkit-csp' || k === 'x-content-security-policy' })[0]; assert.equal(data.headers[cspHeader], \"default-src 'self'; script-src 'self' trusted-cdn.com\"); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8249367417b2b2512c40",
|
||||||
|
"title": "Configure Helmet Using the ‘parent’ helmet() Middleware",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-infosec/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-infosec/'>GitHub</a>.",
|
||||||
|
"app.use(helmet()) will automatically include all the middleware introduced above, except noCache(), and contentSecurityPolicy(), but these can be enabled if necessary. You can also disable or configure any other middleware individually, using a configuration object.",
|
||||||
|
"// Example",
|
||||||
|
"<code>app.use(helmet({</code>",
|
||||||
|
"<code> frameguard: { // configure</code>",
|
||||||
|
"<code> action: 'deny'</code>",
|
||||||
|
"<code> },</code>",
|
||||||
|
"<code> contentSecurityPolicy: { // enable and configure</code>",
|
||||||
|
"<code> directives: {</code>",
|
||||||
|
"<code> defaultSrc: [\"self\"],</code>",
|
||||||
|
"<code> styleSrc: ['style.com'],</code>",
|
||||||
|
"<code> }</code>",
|
||||||
|
"<code> },</code>",
|
||||||
|
"<code> dnsPrefetchControl: false // disable</code>",
|
||||||
|
"<code>}))</code>",
|
||||||
|
"We introduced each middleware separately for teaching purpose, and for ease of testing. Using the ‘parent’ helmet() middleware is easiest, and cleaner, for a real project."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "no tests - it's a descriptive challenge",
|
||||||
|
"testString": "assert(true)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "58a25bcef9fc0f352b528e7c",
|
||||||
|
"title": "Understand BCrypt Hashes",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-bcrypt/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-bcrypt/'>GitHub</a>.",
|
||||||
|
"BCrypt hashes are very secure. A hash is basically a fingerprint of the original data- always unique. This is accomplished by feeding the original data into a algorithm and having returned a fixed length result. To further complicate this process and make it more secure, you can also <em>salt</em> your hash. Salting your hash involves adding random data to the original data before the hashing process which makes it even harder to crack the hash.",
|
||||||
|
"BCrypt hashes will always looks like <code>$2a$13$ZyprE5MRw2Q3WpNOGZWGbeG7ADUre1Q8QO.uUUtcbqloU0yvzavOm</code> which does have a structure. The first small bit of data <code>$2a</code> is defining what kind of hash algorithm was used. The next portion <code>$13</code> defines the <em>cost</em>. Cost is about how much power it takes to compute the hash. It is on a logarithmic scale of 2^cost and determines how many times the data is put through the hashing algorithm. For example, at a cost of 10 you are able to hash 10 passwords a second on an average computer, however at a cost of 15 it takes 3 seconds per hash... and to take it further, at a cost of 31 it would takes multiple days to complete a hash. A cost of 12 is considered very secure at this time. The last portion of your hash <code>$ZyprE5MRw2Q3WpNOGZWGbeG7ADUre1Q8QO.uUUtcbqloU0yvzavOm</code>, looks like 1 large string of numbers, periods, and letters but it is actually 2 separate pieces of information. The first 22 characters is the salt in plain text, and the rest is the hashed password!",
|
||||||
|
"<hr>To begin using BCrypt, add it as a dependency in your project and require it as 'bcrypt' in your server.",
|
||||||
|
"Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "BCyrpt is a dependency",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/package.json') .then(data => { var packJson = JSON.parse(data); assert.property(packJson.dependencies, 'bcrypt', 'Your project should list \"bcrypt\" as a dependency'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "BCrypt has been properly required",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js').then(data => {assert.match(data, /bcrypt.*=.*require.*('|\")bcrypt('|\")/gi, 'You should correctly require and instanciate socket.io as io.');}, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "58a25bcff9fc0f352b528e7d",
|
||||||
|
"title": "Hash and Compare Passwords Asynchronously",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-bcrypt/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-bcrypt/'>GitHub</a>.",
|
||||||
|
"As hashing is designed to be computationally intensive, it is recommended to do so asyncronously on your server as to avoid blocking incoming connections while you hash. All you have to do to hash a password asynchronous is call <code>bcrypt.hash(myPlaintextPassword, saltRounds, (err, hash) => { /*Store hash in your db*/ });</code>",
|
||||||
|
"<hr>Add this hashing function to your server(we've already defined the variables used in the function for you to use) and log it to the console for you to see! At this point you would normally save the hash to your database.",
|
||||||
|
"Now when you need to figure out if a new input is the same data as the hash you would just use the compare function <code>bcrypt.compare(myPlaintextPassword, hash, (err, res) => { /*res == true or false*/ });</code>. Add this into your existing hash function(since you need to wait for the hash to complete before calling the compare function) after you log the completed hash and log 'res' to the console within the compare. You should see in the console a hash then 'true' is printed! If you change 'myPlaintextPassword' in the compare function to 'someOtherPlaintextPassword' then it should say false.",
|
||||||
|
"<pre>bcrypt.hash('passw0rd!', 13, (err, hash) => {\n console.log(hash); //$2a$12$Y.PHPE15wR25qrrtgGkiYe2sXo98cjuMCG1YwSI5rJW1DSJp0gEYS\n bcrypt.compare('passw0rd!', hash, (err, res) => {\n console.log(res); //true\n });\n});</pre>",
|
||||||
|
"Submit your page when you think you've got it right."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Async hash generated and correctly compared",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /START_ASYNC[^]*bcrypt.hash.*myPlaintextPassword( |),( |)saltRounds( |),( |).*err( |),( |)hash[^]*END_ASYNC/gi, 'You should call bcrypt.hash on myPlaintextPassword and saltRounds and handle err and hash as a result in the callback'); assert.match(data, /START_ASYNC[^]*bcrypt.hash[^]*bcrypt.compare.*myPlaintextPassword( |),( |)hash( |),( |).*err( |),( |)res[^]*}[^]*}[^]*END_ASYNC/gi, 'Nested within the hash function should be the compare function comparing myPlaintextPassword to hash'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "58a25bcff9fc0f352b528e7e",
|
||||||
|
"title": "Hash and Compare Passwords Synchronously",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-bcrypt/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-bcrypt/'>GitHub</a>.",
|
||||||
|
"Hashing synchronously is just as easy to do but can cause lag if using it server side with a high cost or with hashing done very often. Hashing with this method is as easy as calling <code>var hash = bcrypt.hashSync(myPlaintextPassword, saltRounds);</code>",
|
||||||
|
"<hr>Add this method of hashing to your code and then log the result to the console. Again, the variables used are already defined in the server so you wont need to adjust them. You may notice even though you are hashing the same password as in the async function, the result in the console is different- this is due to the salt being randomly generated each time as seen by the first 22 characters in the third string of the hash.",
|
||||||
|
"Now to compare a password input with the new sync hash, you would use the compareSync method: <code>var result = bcrypt.compareSync(myPlaintextPassword, hash);</code> with the result being a boolean true or false. Add this function in and log to the console the result to see it working.",
|
||||||
|
"Submit your page when you think you've got it right. If you ran into errors during these challenges you can take a look at the example completed code <a href='https://gist.github.com/JosephLivengood/9a2698fb63e42d9d8b4b84235c08b4c4'>here</a>."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Sync hash generated and correctly compared",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /START_SYNC[^]*hash.*=.*bcrypt.hashSync.*myPlaintextPassword( |),( |)saltRounds[^]*END_SYNC/gi, 'You should call bcrypt.hashSync on myPlaintextPassword with saltRounds'); assert.match(data, /START_SYNC[^]*result.*=.*bcrypt.compareSync.*myPlaintextPassword( |),( |)hash[^]*END_SYNC/gi, 'You should call bcrypt.compareSync on myPlaintextPassword with the hash generated in the last line'); }, xhr => { throw new Error(xhr.statusText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,298 @@
|
|||||||
|
{
|
||||||
|
"name": "Information Security and Quality Assurance Projects",
|
||||||
|
"order": 4,
|
||||||
|
"time": "150 hours",
|
||||||
|
"helpRoom": "HelpBackend",
|
||||||
|
"challenges": [
|
||||||
|
{
|
||||||
|
"id": "587d8249367417b2b2512c41",
|
||||||
|
"title": "Metric-Imperial Converter",
|
||||||
|
"description": [
|
||||||
|
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://hard-twilight.glitch.me/' target='_blank'>https://hard-twilight.glitch.me/</a>.",
|
||||||
|
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicaly visible for our testing.",
|
||||||
|
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-metricimpconverter/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-metricimpconverter/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "I will prevent the client from trying to guess(sniff) the MIME type.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I will prevent cross-site scripting (XSS) attacks.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can GET /api/convert with a single parameter containing an accepted number and unit and have it converted. (Hint: Split the input by looking for the index of the first character which will mark the start of the unit)",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can convert 'gal' to 'L' and vice versa. (1 gal to 3.78541 L)",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can convert 'lbs' to 'kg' and vice versa. (1 lbs to 0.453592 kg)",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can convert 'mi' to 'km' and vice versa. (1 mi to 1.60934 km)",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "If my unit of measurement is invalid, returned will be 'invalid unit'.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "If my number is invalid, returned with will 'invalid number'.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "If both are invalid, return will be 'invalid number and unit'.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can use fractions, decimals or both in my parameter(ie. 5, 1/2, 2.5/6), but if nothing is provided it will default to 1.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "My return will consist of the initNum, initUnit, returnNum, returnUnit, and string spelling out units in format '{initNum} {initial_Units} converts to {returnNum} {return_Units}' with the result rounded to 5 decimals in the string.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "All 16 unit tests are complete and passing.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "All 5 functional tests are complete and passing.",
|
||||||
|
"testString": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"isRequired": true,
|
||||||
|
"releasedOn": "January 15, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8249367417b2b2512c42",
|
||||||
|
"title": "Issue Tracker",
|
||||||
|
"description": [
|
||||||
|
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://protective-garage.glitch.me/' target='_blank'>https://protective-garage.glitch.me/</a>.",
|
||||||
|
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicaly visible for our testing.",
|
||||||
|
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-issuetracker/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-issuetracker/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Prevent cross site scripting (XSS) attacks.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can POST /api/issues/{projectname} with form data containing required issue_title, issue_text, created_by, and optional assigned_to and status_text.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "The object saved (and returned) will include all of those fields (blank for optional no input) and also include created_on(date/time), updated_on(date/time), open(boolean, true for open, false for closed), and _id.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can PUT /api/issues/{projectname} with a id and any fields in the object with a value to object said object. Returned will be 'successfully updated' or 'could not update '+id. This should always update updated_on. If no fields are sent return 'no updated field sent'.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can DELETE /api/issues/{projectname} with a id to completely delete an issue. If no _id is sent return 'id error', success: 'deleted '+id, failed: 'could not delete '+id.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can GET /api/issues/{projectname} for an array of all issues on that specific project with all the information for each issue as was returned when posted.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can filter my get request by also passing along any field and value in the query(ie. /api/issues/{project}?open=false). I can pass along as many fields/values as I want.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "All 11 functional tests are complete and passing.",
|
||||||
|
"testString": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"isRequired": true,
|
||||||
|
"releasedOn": "January 15, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824a367417b2b2512c43",
|
||||||
|
"title": "Personal Library",
|
||||||
|
"description": [
|
||||||
|
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://spark-cathedral.glitch.me/' target='_blank'>https://spark-cathedral.glitch.me/</a>.",
|
||||||
|
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but must be publicaly visible for our testing.",
|
||||||
|
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-library/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-library/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Nothing from my website will be cached in my client.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "The headers will say that the site is powered by 'PHP 4.2.0' even though it isn't (as a security measure).",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can post a title to /api/books to add a book and returned will be the object with the title and a unique _id.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can get /api/books to retrieve an array of all books containing title, _id, and commentcount.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can get /api/books/{id} to retrieve a single object of a book containing _title, _id, & an array of comments (empty array if no comments present).",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can post a comment to /api/books/{id} to add a comment to a book and returned will be the books object similar to get /api/books/{id} including the new comment.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can delete /api/books/{_id} to delete a book from the collection. Returned will be 'delete successful' if successful.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "If I try to request a book that doesn't exist I will be returned 'no book exists'.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can send a delete request to /api/books to delete all books in the database. Returned will be 'complete delete successful' if successful.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "All 6 functional tests required are complete and passing.",
|
||||||
|
"testString": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"isRequired": true,
|
||||||
|
"releasedOn": "January 15, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824a367417b2b2512c44",
|
||||||
|
"title": "Stock Price Checker",
|
||||||
|
"description": [
|
||||||
|
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://giant-chronometer.glitch.me/' target='_blank'>https://giant-chronometer.glitch.me/</a>.",
|
||||||
|
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but must be publicaly visible for our testing.",
|
||||||
|
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-stockchecker/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-stockchecker/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Set the content security policies to only allow loading of scripts and css from your server.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can GET /api/stock-prices with form data containing a Nasdaq stock ticker and recieve back an object stockData.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "In stockData, I can see the stock(string, the ticker), price(decimal in string format), and likes(int).",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can also pass along field like as true(boolean) to have my like added to the stock(s). Only 1 like per ip should be accepted.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "If I pass along 2 stocks, the return object will be an array with both stock's info. Instead of likes, it will display rel_likes(the difference between the likes on both stocks) on both.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "A good way to receive current price is the following external API(replacing 'GOOG' with your stock): https://finance.google.com/finance/info?q=NASDAQ%3aGOOG",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "All 5 functional tests are complete and passing.",
|
||||||
|
"testString": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"isRequired": true,
|
||||||
|
"releasedOn": "January 15, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824a367417b2b2512c45",
|
||||||
|
"title": "Anonymous Message Board",
|
||||||
|
"description": [
|
||||||
|
"Build a full stack JavaScript app that is functionally similar to this: <a href='https://horn-celery.glitch.me/' target='_blank'>https://horn-celery.glitch.me/</a>.",
|
||||||
|
"Working on this project will involve you writing your code on Glitch on our starter project. After completing this project you can copy your public glitch url (to the homepage of your app) into this screen to test it! Optionally you may choose to write your project on another platform but it must be publicaly visible for our testing.",
|
||||||
|
"Start this project on Glitch using <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-project-messageboard/'>this link</a> or clone <a href='https://github.com/freeCodeCamp/boilerplate-project-messageboard/'>this repository</a> on GitHub! If you use Glitch, remember to save the link to your project somewhere safe!"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "Only allow your site to be loading in an iFrame on your own pages.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Do not allow DNS prefetching.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Only allow your site to send the referrer for your own pages.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can POST a thread to a specific message board by passing form data text and deletepassword_ to /api/threads/{board}.(Recommend res.redirect to board page /b/{board}) Saved will be at least _id, text, createdon_(date&time), bumpedon_(date&time, starts same as created_on), reported(boolean), deletepassword_, & replies(array).",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can POST a reply to a thread on a specific board by passing form data text, deletepassword_, & threadid_ to /api/replies/{board} and it will also update the bumped_on date to the comments date.(Recommend res.redirect to thread page /b/{board}/{thread_id}) In the thread's replies array will be saved _id, text, createdon_, deletepassword_, & reported.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can GET an array of the most recent 10 bumped threads on the board with only the most recent 3 replies each from /api/threads/{board}. The reported and deletepasswords_ fields will not be sent to the client.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can GET an entire thread with all its replies from /api/replies/{board}?thread_id={thread_id}. Also hiding the same fields the client should be see.",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can delete a thread completely if I send a DELETE request to /api/threads/{board} and pass along the threadid_ & deletepassword_. (Text response will be 'incorrect password' or 'success')",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can delete a post(just changing the text to '[deleted]' instead of removing completely like a thread) if I send a DELETE request to /api/replies/{board} and pass along the threadid_, replyid_, & deletepassword_. (Text response will be 'incorrect password' or 'success')",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can report a thread and change its reported value to true by sending a PUT request to /api/threads/{board} and pass along the threadid_. (Text response will be 'success')",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I can report a reply and change its reported value to true by sending a PUT request to /api/replies/{board} and pass along the threadid_ & replyid_. (Text response will be 'success')",
|
||||||
|
"testString": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Complete functional tests that wholly test routes and pass.",
|
||||||
|
"testString": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"isRequired": true,
|
||||||
|
"releasedOn": "January 15, 2017",
|
||||||
|
"translations": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,815 @@
|
|||||||
|
{
|
||||||
|
"name": "Quality Assurance and Testing with Chai",
|
||||||
|
"order": 2,
|
||||||
|
"time": "5 hours",
|
||||||
|
"helpRoom": "Help",
|
||||||
|
"challenges": [
|
||||||
|
{
|
||||||
|
"id": "587d824a367417b2b2512c46",
|
||||||
|
"title": "Learn How JavaScript Assertions Work",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"Use assert.isNull() or assert.isNotNull() to make the tests pass."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=0').then(data => {assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isNull vs. isNotNull",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=0').then(data => { assert.equal(data.assertions[0].method, 'isNull', 'Null is null'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isNull vs. isNotNull",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=0').then(data => { assert.equal(data.assertions[1].method, 'isNotNull', '1 is not null'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824b367417b2b2512c47",
|
||||||
|
"title": "Test if a Variable or Function is Defined",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"Use assert.isDefined() or assert.isUndefined() to make the tests pass"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=1').then(data => {assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isDefined vs. isUndefined",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=1').then(data => { assert.equal(data.assertions[0].method, 'isDefined', 'Null is not undefined'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isDefined vs. isUndefined",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=1').then(data => { assert.equal(data.assertions[1].method, 'isUndefined', 'Undefined is undefined'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isDefined vs. isUndefined",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=1').then(data => { assert.equal(data.assertions[2].method, 'isDefined', 'A string is not undefined'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824b367417b2b2512c48",
|
||||||
|
"title": "Use Assert.isOK and Assert.isNotOK",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"Use assert.isOk() or assert.isNotOk() to make the tests pass.",
|
||||||
|
".isOk(truthy) and .isNotOk(falsey) will pass."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=2').then(data => {assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isOk vs. isNotOk",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=2').then(data => { assert.equal(data.assertions[0].method, 'isNotOk', 'Null is falsey'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isOk vs. isNotOk",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=2').then(data => { assert.equal(data.assertions[1].method, 'isOk','A string is truthy'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isOk vs. isNotOk",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=2').then(data => { assert.equal(data.assertions[2].method, 'isOk', 'true is truthy'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824b367417b2b2512c49",
|
||||||
|
"title": "Test for Truthiness",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"Use assert.isTrue() or assert.isNotTrue() to make the tests pass.",
|
||||||
|
".isTrue(true) and .isNotTrue(everything else) will pass.",
|
||||||
|
".isFalse() and .isNotFalse() also exist."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=3').then(data => {assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isTrue vs. isNotTrue",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=3').then(data => { assert.equal(data.assertions[0].method, 'isTrue', 'True is true'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isTrue vs. isNotTrue",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=3').then(data => { assert.equal(data.assertions[1].method, 'isTrue', 'Double negation of a truthy value is true'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isTrue vs. isNotTrue",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=3').then(data => { assert.equal(data.assertions[2].method, 'isNotTrue', 'A truthy object is not true - neither is a false one'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824b367417b2b2512c4a",
|
||||||
|
"title": "Use the Double Equals to Assert Equality",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
".equal(), .notEqual()",
|
||||||
|
".equal() compares objects using '=='"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=4').then(data => {assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - equal vs. notEqual",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=4').then(data => { assert.equal(data.assertions[0].method, 'equal', 'Numbers are coerced into strings with == '); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - equal vs. notEqual",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=4').then(data => { assert.equal(data.assertions[1].method, 'notEqual', ' == compares object references'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - equal vs. notEqual",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=4').then(data => { assert.equal(data.assertions[2].method, 'equal', '6 * \\'2\\' is 12 ! It should be equal to \\'12\\''); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - equal vs. notEqual",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=4').then(data => { assert.equal(data.assertions[3].method, 'notEqual', '6 + \\'2\\' is \\'62\\'...'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824b367417b2b2512c4b",
|
||||||
|
"title": "Use the Triple Equals to Assert Strict Equality",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
".strictEqual(), .notStrictEqual()",
|
||||||
|
".strictEqual() compares objects using '==='"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=5').then(data => {assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - strictEqual vs. notStrictEqual",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=5').then(data => { assert.equal(data.assertions[0].method, 'notStrictEqual', 'with strictEqual the type must match'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - strictEqual vs. notStrictEqual",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=5').then(data => { assert.equal(data.assertions[1].method, 'strictEqual', '3*2 = 6...'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - strictEqual vs. notStrictEqual",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=5').then(data => { assert.equal(data.assertions[2].method, 'strictEqual', '6 * \\'2\\' is 12. Types match !'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - strictEqual vs. notStrictEqual",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=5').then(data => { assert.equal(data.assertions[3].method, 'notStrictEqual', 'Even if they have the same elements, the Arrays are notStrictEqual'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824c367417b2b2512c4c",
|
||||||
|
"title": "Assert Deep Equality with .deepEqual and .notDeepEqual",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
".deepEqual(), .notDeepEqual()",
|
||||||
|
".deepEqual() asserts that two object are deep equal"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=6').then(data => {assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - deepEqual vs. notDeepEqual",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=6').then(data => { assert.equal(data.assertions[0].method, 'deepEqual', 'The order of the keys does not matter'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - deepEqual vs. notDeepEqual",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=6').then(data => { assert.equal(data.assertions[1].method, 'notDeepEqual', 'The position of elements within an array does matter'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824c367417b2b2512c4d",
|
||||||
|
"title": "Compare the Properties of Two Elements",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
".isAbove() => a > b , .isAtMost() => a <= b"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=7').then(data => {assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isAbove vs. isAtMost",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=7').then(data => { assert.equal(data.assertions[0].method, 'isAtMost', '5 is at most (<=) 5'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isAbove vs. isAtMost",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=7').then(data => { assert.equal(data.assertions[1].method, 'isAbove', '1 is greater than 0'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isAbove vs. isAtMost",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=7').then(data => { assert.equal(data.assertions[2].method, 'isAbove', 'Math.PI = 3.14159265 is greater than 3'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isAbove vs. isAtMost",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=7').then(data => { assert.equal(data.assertions[3].method, 'isAtMost', '1 - Math.random() is > 0 and <= 1. It is atMost 1 !'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824c367417b2b2512c4e",
|
||||||
|
"title": "Test if One Value is Below or At Least as Large as Another",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
".isBelow() => a < b , .isAtLeast => a >= b"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=8').then(data => {assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isBelow vs. isAtLeast",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=8').then(data => { assert.equal(data.assertions[0].method, 'isAtLeast', '5 is at least (>=) 5'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isBelow vs. isAtLeast",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=8').then(data => { assert.equal(data.assertions[1].method, 'isAtLeast', '2 * Math.random() is at least 0'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isBelow vs. isAtLeast",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=8').then(data => { assert.equal(data.assertions[2].method, 'isBelow', '1 is smaller than 2'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isBelow vs. isAtLeast",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=8').then(data => { assert.equal(data.assertions[3].method, 'isBelow', '2/3 (0.6666) is smaller than 1'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824c367417b2b2512c4f",
|
||||||
|
"title": "Test if a Value Falls within a Specific Range",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
".approximately",
|
||||||
|
".approximately(actual, expected, range, [message])",
|
||||||
|
"actual = expected +/- range",
|
||||||
|
"Choose the minimum range (3rd parameter) to make the test always pass",
|
||||||
|
"it should be less than 1"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=10').then(data => {assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Use approximately(actual, expected, range) - Chose the correct range",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=10').then(data => { assert.equal(data.assertions[0].method, 'isApproximately', 'weirdNumbers(0.5) is in the range (0.5, 1.5]. It\\'s within 1 +/- 0.5'); assert.equal(data.assertions[0].args[2], 0.5); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Use approximately(actual, expected, range) - Chose the correct range",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=10').then(data => { assert.equal(data.assertions[1].method, 'isApproximately'); assert.equal(data.assertions[1].args[2], 0.8, 'weirdNumbers(0.2) is in the range (0.2, 1.2] It\\'s within 1 +/- 0.8'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824d367417b2b2512c50",
|
||||||
|
"title": "Test if a Value is an Array",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=10').then(data => {assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isArray vs. isNotArray",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=10').then(data => { assert.equal(data.assertions[0].method, 'isArray', 'String.prototype.split() returns an Array'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isArray vs. isNotArray",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=10').then(data => { assert.equal(data.assertions[1].method, 'isNotArray', 'Array.prototype.indexOf() returns a number'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824d367417b2b2512c51",
|
||||||
|
"title": "Test if an Array Contains an Item",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=11').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - include vs. notInclude",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=11').then(data => { assert.equal(data.assertions[0].method, 'notInclude', 'It\\'s summer in july...'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - include vs. notInclude",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=11').then(data => { assert.equal(data.assertions[1].method, 'include', 'JavaScript is a backend language !!'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824d367417b2b2512c52",
|
||||||
|
"title": "Test if a Value is a String",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"#isString asserts that the actual value is a string."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=12').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isString vs. isNotString",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=12').then(data => { assert.equal(data.assertions[0].method, 'isNotString', 'A float number is not a string'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isString vs. isNotString",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=12').then(data => { assert.equal(data.assertions[1].method, 'isString', 'environment vars are strings (or undefined)'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - isString vs. isNotString",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=12').then(data => { assert.equal(data.assertions[2].method, 'isString', 'A JSON is a string'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824d367417b2b2512c53",
|
||||||
|
"title": "Test if a String Contains a Substring",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"#include (on #notInclude ) works for strings too !!",
|
||||||
|
"It asserts that the actual string contains the expected substring"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=13').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - include vs. notInclude",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=13').then(data => { assert.equal(data.assertions[0].method, 'include', '\\'Arrow\\' contains \\'row\\'...'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - include vs. notInclude",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=13').then(data => { assert.equal(data.assertions[1].method, 'notInclude', '... a \\'dart\\' doesn\\'t contain a \\'queue\\''); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824d367417b2b2512c54",
|
||||||
|
"title": "Use Regular Expressions to Test a String",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"#match Asserts that the actual value",
|
||||||
|
"matches the second argument regular expression."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=14').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - match vs. notMatch",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=14').then(data => { assert.equal(data.assertions[0].method, 'match', '\\'# name: John Doe, age: 35\\' matches the regex'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - match vs. notMatch",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=14').then(data => { assert.equal(data.assertions[1].method, 'notMatch', '\\'# name: Paul Smith III, age: twenty-four\\' does not match the regex (the age must be numeric)'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824e367417b2b2512c55",
|
||||||
|
"title": "Test if an Object has a Property",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"#property asserts that the actual object has a given property.",
|
||||||
|
"Use #property or #notProperty where appropriate"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=15').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - property vs. notProperty",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=15').then(data => { assert.equal(data.assertions[0].method, 'notProperty', 'A car has not wings'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - property vs. notProperty",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=15').then(data => { assert.equal(data.assertions[1].method, 'property', 'planes have engines'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - property vs. notProperty",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=15').then(data => { assert.equal(data.assertions[2].method, 'property', 'Cars have wheels'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824e367417b2b2512c56",
|
||||||
|
"title": "Test if a Value is of a Specific Data Structure Type",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"#typeOf asserts that value’s type is the given string, as determined by Object.prototype.toString.",
|
||||||
|
"Use #typeOf or #notTypeOf where appropriate"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=16').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - typeOf vs. notTypeOf",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=16').then(data => { assert.equal(data.assertions[0].method, 'typeOf', 'myCar is typeOf Object'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - typeOf vs. notTypeOf",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=16').then(data => { assert.equal(data.assertions[1].method, 'typeOf', 'Car.model is a String'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - typeOf vs. notTypeOf",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=16').then(data => { assert.equal(data.assertions[1].method, 'notTypeOf', 'Plane.wings is a Number (not a String)'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - typeOf vs. notTypeOf",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=16').then(data => { assert.equal(data.assertions[3].method, 'typeOf', 'Plane.engines is an Array'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - typeOf vs. notTypeOf",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=16').then(data => { assert.equal(data.assertions[4].method, 'typeOf', 'Car.wheels is a Number'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824e367417b2b2512c57",
|
||||||
|
"title": "Test if an Object is an Instance of a Constructor",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"#instanceOf asserts that an object is an instance of a constructor.",
|
||||||
|
"Use #instanceOf or #notInstanceOf where appropriate"
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=17').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - instanceOf vs. notInstanceOf",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=17').then(data => { assert.equal(data.assertions[0].method, 'notInstanceOf', 'myCar is not an instance of Plane'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - instanceOf vs. notInstanceOf",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=17').then(data => { assert.equal(data.assertions[1].method, 'instanceOf', 'airlinePlane is an instance of Plane'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - instanceOf vs. notInstanceOf",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=17').then(data => { assert.equal(data.assertions[2].method, 'instanceOf', 'everything is an Object in JavaScript...'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Choose the right assertion - instanceOf vs. notInstanceOf",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=unit&n=17').then(data => { assert.equal(data.assertions[3].method, 'notInstanceOf', 'myCar.wheels is not an instance of String'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824e367417b2b2512c58",
|
||||||
|
"title": "Run Functional Tests on API Endpoints using Chai-HTTP",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"Replace assert.fail(). Test the status and the text.response. Make the test pass.",
|
||||||
|
"Don't send a name in the query, the endpoint with responds with 'hello Guest'."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=0').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "You should test for 'res.status' == 200",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=0').then(data => { assert.equal(data.assertions[0].method, 'equal'); assert.equal(data.assertions[0].args[0], 'res.status'); assert.equal(data.assertions[0].args[1], '200');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "You should test for 'res.text' == 'hello Guest'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=0').then(data => { assert.equal(data.assertions[1].method, 'equal'); assert.equal(data.assertions[1].args[0], 'res.text'); assert.equal(data.assertions[1].args[1], '\\'hello Guest\\'');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824f367417b2b2512c59",
|
||||||
|
"title": "Run Functional Tests on API Endpoints using Chai-HTTP II",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"Replace assert.fail(). Test the status and the text.response. Make the test pass.",
|
||||||
|
"Send you name in the query appending ?name=<your_name>, the endpoint with responds with 'hello <your_name>'."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=1').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "You should test for 'res.status' == 200",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=1').then(data => { assert.equal(data.assertions[0].method, 'equal'); assert.equal(data.assertions[0].args[0], 'res.status'); assert.equal(data.assertions[0].args[1], '200');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "You should test for 'res.text' == 'hello Guest'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=1').then(data => { assert.equal(data.assertions[1].method, 'equal'); assert.equal(data.assertions[1].args[0], 'res.text'); assert.match(data.assertions[1].args[1], /hello [\\w\\d_-]/);}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824f367417b2b2512c5a",
|
||||||
|
"title": "Run Functional Tests on an API Response using Chai-HTTP III - PUT method",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"In the next example we'll see how to send data in a request payload (body).",
|
||||||
|
"We are going to test a PUT request. The '/travellers' endpoint accepts",
|
||||||
|
"a JSON object taking the structure :",
|
||||||
|
" {surname: [last name of a traveller of the past]} ,",
|
||||||
|
"The route responds with :",
|
||||||
|
" {name: [first name], surname:[last name], dates: [birth - death years]}",
|
||||||
|
"see the server code for more details.",
|
||||||
|
"Send {surname: 'Colombo'}. Replace assert.fail() and make the test pass.",
|
||||||
|
"Check for 1) status, 2) type, 3) body.name, 4) body.surname",
|
||||||
|
"Follow the assertion order above, We rely on it."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=2').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "You should test for 'res.status' to be 200",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=2').then(data => { assert.equal(data.assertions[0].method, 'equal'); assert.equal(data.assertions[0].args[0], 'res.status'); assert.equal(data.assertions[0].args[1], '200');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "You should test for 'res.type' to be 'application/json'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=2').then(data => { assert.equal(data.assertions[1].method, 'equal'); assert.equal(data.assertions[1].args[0], 'res.type'); assert.equal(data.assertions[1].args[1], '\\'application/json\\'');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "You should test for 'res.body.name' to be 'Cristoforo'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=2').then(data => { assert.equal(data.assertions[2].method, 'equal'); assert.equal(data.assertions[2].args[0], 'res.body.name'); assert.equal(data.assertions[2].args[1], '\\'Cristoforo\\'');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "You should test for 'res.body.surname' to be 'Colombo'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=2').then(data => { assert.equal(data.assertions[3].method, 'equal'); assert.equal(data.assertions[3].args[0], 'res.body.surname'); assert.equal(data.assertions[3].args[1], '\\'Colombo\\'');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824f367417b2b2512c5b",
|
||||||
|
"title": "Run Functional Tests on an API Response using Chai-HTTP IV - PUT method redux",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"This exercise is similar to the preceding. Look at it for the details.",
|
||||||
|
"Send {surname: 'da Verrazzano'}. Replace assert.fail() and make the test pass.",
|
||||||
|
"Check for 1) status, 2) type, 3) body.name, 4) body.surname",
|
||||||
|
"Follow the assertion order above, We rely on it."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=3').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "You should test for 'res.status' to be 200",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=3').then(data => { assert.equal(data.assertions[0].method, 'equal'); assert.equal(data.assertions[0].args[0], 'res.status'); assert.equal(data.assertions[0].args[1], '200');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "You should test for 'res.type' to be 'application/json'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=3').then(data => { assert.equal(data.assertions[1].method, 'equal'); assert.equal(data.assertions[1].args[0], 'res.type'); assert.equal(data.assertions[1].args[1], '\\'application/json\\'');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "You should test for 'res.body.name' to be 'Giovanni'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=3').then(data => { assert.equal(data.assertions[2].method, 'equal'); assert.equal(data.assertions[2].args[0], 'res.body.name'); assert.equal(data.assertions[2].args[1], '\\'Giovanni\\'');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "You should test for 'res.body.surname' to be 'da Verrazzano'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=3').then(data => { assert.equal(data.assertions[3].method, 'equal'); assert.equal(data.assertions[3].args[0], 'res.body.surname'); assert.equal(data.assertions[3].args[1], '\\'da Verrazzano\\'');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d824f367417b2b2512c5c",
|
||||||
|
"title": "Run Functional Tests using a Headless Browser",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"In the next challenges we are going to simulate the human interaction with a page using a device called 'Headless Browser'.",
|
||||||
|
"A headless browser is a web browser without a graphical user interface. These kind of tools are particularly useful for testing web pages as they are able to render and understand HTML, CSS, and JavaScript the same way a browser would.",
|
||||||
|
"For these challenges we are using Zombie.JS. It's a lightweight browser which is totally based on JS, without relying on additional binaries to be installed. This feature makes it usable in an environment such as Glitch. There are many other (more powerful) options.<br>",
|
||||||
|
"Look at the examples in the code for the exercise directions Follow the assertions order, We rely on it."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=4').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "assert that the headless browser request succeded",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=4').then(data => { assert.equal(data.assertions[0].method, 'browser.success'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "assert that the text inside the element 'span#name' is 'Cristoforo'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=4').then(data => { assert.equal(data.assertions[1].method, 'browser.text'); assert.equal(data.assertions[1].args[0], '\\'span#name\\''); assert.equal(data.assertions[1].args[1], '\\'Cristoforo\\'');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "assert that the text inside the element 'span#surname' is 'Colombo'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=4').then(data => { assert.equal(data.assertions[2].method, 'browser.text'); assert.equal(data.assertions[2].args[0], '\\'span#surname\\''); assert.equal(data.assertions[2].args[1], '\\'Colombo\\'');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "assert that the element 'span#dates' exist and its count is 1",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=4').then(data => { assert.equal(data.assertions[3].method, 'browser.element'); assert.equal(data.assertions[3].args[0], '\\'span#dates\\''); assert.equal(data.assertions[3].args[1], 1);}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "587d8250367417b2b2512c5d",
|
||||||
|
"title": "Run Functional Tests using a Headless Browser II",
|
||||||
|
"description": [
|
||||||
|
"As a reminder, this project is being built upon the following starter project on <a href='https://glitch.com/#!/import/github/freeCodeCamp/boilerplate-mochachai/'>Glitch</a>, or cloned from <a href='https://github.com/freeCodeCamp/boilerplate-mochachai/'>GitHub</a>.",
|
||||||
|
"This exercise is similar to the preceding.",
|
||||||
|
"Look at the code for directions. Follow the assertions order, We rely on it."
|
||||||
|
],
|
||||||
|
"challengeSeed": [],
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"text": "All tests should pass",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=5').then(data => { assert.equal(data.state,'passed'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " assert that the headless browser request succeded",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=5').then(data => { assert.equal(data.assertions[0].method, 'browser.success'); }, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "assert that the text inside the element 'span#name' is 'Amerigo'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=5').then(data => { assert.equal(data.assertions[1].method, 'browser.text'); assert.equal(data.assertions[1].args[0], '\\'span#name\\''); assert.equal(data.assertions[1].args[1], '\\'Amerigo\\'');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "assert that the text inside the element 'span#surname' is 'Vespucci'",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=5').then(data => { assert.equal(data.assertions[2].method, 'browser.text'); assert.equal(data.assertions[2].args[0], '\\'span#surname\\''); assert.equal(data.assertions[2].args[1], '\\'Vespucci\\'');}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "assert that the element 'span#dates' exist and its count is 1",
|
||||||
|
"testString": "getUserInput => $.get(getUserInput('url') + '/_api/get-tests?type=functional&n=5').then(data => { assert.equal(data.assertions[3].method, 'browser.element'); assert.equal(data.assertions[3].args[0], '\\'span#dates\\''); assert.equal(data.assertions[3].args[1], 1);}, xhr => { throw new Error(xhr.responseText); })"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solutions": [],
|
||||||
|
"hints": [],
|
||||||
|
"challengeType": 2,
|
||||||
|
"releasedOn": "Feb 17, 2017",
|
||||||
|
"translations": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
111
packages/learn/src/auth/index.js
Normal file
111
packages/learn/src/auth/index.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/* global AUTH0_DOMAIN AUTH0_CLIENT_ID AUTH0_NAMESPACE*/
|
||||||
|
import auth0 from 'auth0-js/build/auth0';
|
||||||
|
import { navigateTo } from 'gatsby-link';
|
||||||
|
|
||||||
|
const namespace = AUTH0_NAMESPACE;
|
||||||
|
const domain = AUTH0_DOMAIN;
|
||||||
|
const clientID = AUTH0_CLIENT_ID;
|
||||||
|
|
||||||
|
class Auth {
|
||||||
|
constructor() {
|
||||||
|
this.auth0 = new auth0.WebAuth({
|
||||||
|
domain,
|
||||||
|
clientID,
|
||||||
|
redirectUri: `${
|
||||||
|
typeof window !== 'undefined' ? window.location.origin : ''
|
||||||
|
}/auth-callback`,
|
||||||
|
audience: `https://${domain}/api/v2/`,
|
||||||
|
responseType: 'token id_token',
|
||||||
|
scope: `openid profile email ${namespace + 'accountLinkId'}`
|
||||||
|
});
|
||||||
|
|
||||||
|
this.getUser = this.getUser.bind(this);
|
||||||
|
this.getToken = this.getToken.bind(this);
|
||||||
|
this.handleAuthentication = this.handleAuthentication.bind(this);
|
||||||
|
this.isAuthenticated = this.isAuthenticated.bind(this);
|
||||||
|
this.login = this.login.bind(this);
|
||||||
|
this.logout = this.logout.bind(this);
|
||||||
|
this.setSession = this.setSession.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
login() {
|
||||||
|
return this.auth0.authorize();
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
return Promise.all([
|
||||||
|
localStorage.removeItem('access_token'),
|
||||||
|
localStorage.removeItem('id_token'),
|
||||||
|
localStorage.removeItem('expires_at'),
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
]).then(
|
||||||
|
() =>
|
||||||
|
typeof window !== 'undefined'
|
||||||
|
? window.location.reload()
|
||||||
|
: navigateTo('/#')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAuthentication({ updateUserSignedIn, fetchUserComplete }) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
this.auth0.parseHash((err, authResult) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(err);
|
||||||
|
return navigateTo('/strange-place');
|
||||||
|
}
|
||||||
|
if (authResult && authResult.accessToken && authResult.idToken) {
|
||||||
|
return (
|
||||||
|
this.setSession(authResult)
|
||||||
|
.then(user => {
|
||||||
|
updateUserSignedIn(true);
|
||||||
|
return fetchUserComplete(user);
|
||||||
|
})
|
||||||
|
// this could be current-challenge
|
||||||
|
.then(() => navigateTo('/#'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return navigateTo('/strange-place');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthenticated() {
|
||||||
|
const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
|
||||||
|
const isAuth = new Date().getTime() < expiresAt;
|
||||||
|
return isAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSession = authResult => {
|
||||||
|
const expiresAt = JSON.stringify(
|
||||||
|
authResult.expiresIn * 1000 + new Date().getTime()
|
||||||
|
);
|
||||||
|
localStorage.setItem('access_token', authResult.accessToken);
|
||||||
|
localStorage.setItem('id_token', authResult.idToken);
|
||||||
|
localStorage.setItem('expires_at', expiresAt);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.auth0.client.userInfo(authResult.accessToken, (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
// TODO: Decide what we want to do here
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
localStorage.setItem('user', JSON.stringify(user));
|
||||||
|
resolve(user);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getUser() {
|
||||||
|
const user = this.isAuthenticated() && localStorage.getItem('user');
|
||||||
|
if (user) {
|
||||||
|
return JSON.parse(user);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken() {
|
||||||
|
return localStorage.getItem('id_token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auth = new Auth();
|
20
packages/learn/src/components/Header/components/Login.js
Normal file
20
packages/learn/src/components/Header/components/Login.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Button from 'react-bootstrap/lib/Button';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
login: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function Login({ login }) {
|
||||||
|
return (
|
||||||
|
<Button bsStyle='default' onClick={login}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Login.displayName = 'Login';
|
||||||
|
Login.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default Login;
|
27
packages/learn/src/components/Header/components/SignedIn.js
Normal file
27
packages/learn/src/components/Header/components/SignedIn.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
|
||||||
|
import MenuItem from 'react-bootstrap/lib/MenuItem';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
email: PropTypes.string,
|
||||||
|
logout: PropTypes.func.isRequired,
|
||||||
|
name: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
function SignedIn({ email, name, logout }) {
|
||||||
|
return (
|
||||||
|
<DropdownButton
|
||||||
|
bsStyle='default'
|
||||||
|
id='signedin-dropdown-button'
|
||||||
|
title={name ? name : email}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={logout}>Log Out</MenuItem>
|
||||||
|
</DropdownButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SignedIn.displayName = 'SignedIn';
|
||||||
|
SignedIn.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default SignedIn;
|
64
packages/learn/src/components/Header/components/UserState.js
Normal file
64
packages/learn/src/components/Header/components/UserState.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { auth } from '../../../auth';
|
||||||
|
import {
|
||||||
|
fetchUserComplete,
|
||||||
|
isSignedInSelector,
|
||||||
|
userSelector,
|
||||||
|
updateUserSignedIn
|
||||||
|
} from '../../../redux/app';
|
||||||
|
import Login from './Login';
|
||||||
|
import SignedIn from './SignedIn';
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
isSignedInSelector,
|
||||||
|
userSelector,
|
||||||
|
(isSignedIn, { name, email }) => ({ isSignedIn, name, email })
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch =>
|
||||||
|
bindActionCreators({ updateUserSignedIn, fetchUserComplete }, dispatch);
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
email: PropTypes.string,
|
||||||
|
fetchUserComplete: PropTypes.func.isRequired,
|
||||||
|
isSignedIn: PropTypes.bool,
|
||||||
|
name: PropTypes.string,
|
||||||
|
updateUserSignedIn: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
class UserState extends PureComponent {
|
||||||
|
componentDidMount() {
|
||||||
|
const isAuth = auth.isAuthenticated();
|
||||||
|
if (isAuth) {
|
||||||
|
this.props.fetchUserComplete(auth.getUser());
|
||||||
|
}
|
||||||
|
this.props.updateUserSignedIn(isAuth);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const isAuth = auth.isAuthenticated();
|
||||||
|
if (prevProps.isSignedIn && !isAuth) {
|
||||||
|
this.props.fetchUserComplete(auth.getUser());
|
||||||
|
this.props.updateUserSignedIn(isAuth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isSignedIn, name, email } = this.props;
|
||||||
|
return isSignedIn && (name || email) ? (
|
||||||
|
<SignedIn email={email} logout={auth.logout} name={name} />
|
||||||
|
) : (
|
||||||
|
<Login login={auth.login} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UserState.displayName = 'UserState';
|
||||||
|
UserState.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(UserState);
|
@ -6,3 +6,11 @@
|
|||||||
#top-nav img {
|
#top-nav img {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-nav-container {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 960px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
@ -2,16 +2,12 @@ import React from 'react';
|
|||||||
import Link from 'gatsby-link';
|
import Link from 'gatsby-link';
|
||||||
|
|
||||||
import './header.css';
|
import './header.css';
|
||||||
|
import UserState from './components/UserState';
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
return (
|
return (
|
||||||
<header id='top-nav'>
|
<header id='top-nav'>
|
||||||
<div
|
<div className='top-nav-container'>
|
||||||
style={{
|
|
||||||
margin: '0 auto',
|
|
||||||
maxWidth: 960
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
style={{
|
style={{
|
||||||
color: 'white',
|
color: 'white',
|
||||||
@ -25,6 +21,7 @@ function Header() {
|
|||||||
title='freeCodeCamp | Learn to code'
|
title='freeCodeCamp | Learn to code'
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
<UserState />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
27
packages/learn/src/pages/auth-callback.js
Normal file
27
packages/learn/src/pages/auth-callback.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { auth } from '../auth';
|
||||||
|
import { updateUserSignedIn, fetchUserComplete } from '../redux/app';
|
||||||
|
|
||||||
|
const mapStateToProps = () => ({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch =>
|
||||||
|
bindActionCreators({ updateUserSignedIn, fetchUserComplete }, dispatch);
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
fetchUserComplete: PropTypes.func.isRequired,
|
||||||
|
updateUserSignedIn: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function AuthCallback({ updateUserSignedIn, fetchUserComplete }) {
|
||||||
|
auth.handleAuthentication({ updateUserSignedIn, fetchUserComplete });
|
||||||
|
return <h2>One moment whilst we finish signing you in</h2>;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthCallback.displayName = 'AuthCallback';
|
||||||
|
AuthCallback.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(AuthCallback);
|
@ -1,64 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
|
|
||||||
import { signIn } from '../redux/app';
|
|
||||||
|
|
||||||
const mapStateToProps = () => ({});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch =>
|
|
||||||
bindActionCreators(
|
|
||||||
{
|
|
||||||
signIn
|
|
||||||
},
|
|
||||||
dispatch
|
|
||||||
);
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
signIn: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
class SignInPage extends PureComponent {
|
|
||||||
constructor(...props) {
|
|
||||||
super(...props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
userEmail: ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEmailChange = e => {
|
|
||||||
e.persist();
|
|
||||||
return this.setState(state => ({ ...state, userEmail: e.target.value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSubmit = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const { userEmail } = this.state;
|
|
||||||
this.props.signIn(userEmail);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { userEmail } = this.state;
|
|
||||||
console.log(window.location.origin);
|
|
||||||
return (
|
|
||||||
<div id='sigin-view'>
|
|
||||||
<h2>Start coding</h2>
|
|
||||||
<form onSubmit={this.handleSubmit}>
|
|
||||||
<input
|
|
||||||
name='userEmail'
|
|
||||||
onChange={this.handleEmailChange}
|
|
||||||
type='email'
|
|
||||||
value={userEmail}
|
|
||||||
/>
|
|
||||||
<button type='submit'>Submit</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SignInPage.displayName = 'SignInPage';
|
|
||||||
SignInPage.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(SignInPage);
|
|
6
packages/learn/src/pages/strange-place.css
Normal file
6
packages/learn/src/pages/strange-place.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.strange-place-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
34
packages/learn/src/pages/strange-place.js
Normal file
34
packages/learn/src/pages/strange-place.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
// import PropTypes from 'prop-types';
|
||||||
|
import Panel from 'react-bootstrap/lib/Panel';
|
||||||
|
|
||||||
|
import './strange-place.css';
|
||||||
|
|
||||||
|
const propTypes = {};
|
||||||
|
|
||||||
|
function StrangePlace(props) {
|
||||||
|
console.log(props);
|
||||||
|
return (
|
||||||
|
<div className='strange-place-container'>
|
||||||
|
<Panel bsStyle='primary'>
|
||||||
|
<Panel.Heading>
|
||||||
|
<Panel.Title componentClass='h3'>Whoops!</Panel.Title>
|
||||||
|
</Panel.Heading>
|
||||||
|
<Panel.Body>
|
||||||
|
<p>Something really weird happend and we are not sure what.</p>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
Could you help us fix this by supplying annonymous application data
|
||||||
|
for us to look over?
|
||||||
|
</p>
|
||||||
|
<p>We can show you exactly what we would like to send</p>
|
||||||
|
</Panel.Body>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StrangePlace.displayName = 'StrangePlace';
|
||||||
|
StrangePlace.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default StrangePlace;
|
@ -1,41 +1,39 @@
|
|||||||
import { createAction, handleActions } from 'redux-actions';
|
import { createAction, handleActions } from 'redux-actions';
|
||||||
|
|
||||||
import { createTypes } from '../../../utils/stateManagment';
|
import { createTypes } from '../../../utils/stateManagment';
|
||||||
import signInEpic from './sign-in-epic';
|
|
||||||
|
|
||||||
const ns = 'app';
|
const ns = 'app';
|
||||||
|
|
||||||
export const epics = [signInEpic];
|
export const epics = [];
|
||||||
|
|
||||||
export const types = createTypes(
|
export const types = createTypes(
|
||||||
[
|
['fetchUser', 'fetchUserComplete', 'fetchUserError', 'updateUserSignedIn'],
|
||||||
'fetchUser',
|
|
||||||
'fetchUserComplete',
|
|
||||||
'fetchUserError',
|
|
||||||
'signIn',
|
|
||||||
'signInComplete',
|
|
||||||
'signInError'
|
|
||||||
],
|
|
||||||
ns
|
ns
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
isSignedIn: false,
|
||||||
user: {}
|
user: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const signIn = createAction(types.signIn);
|
|
||||||
export const signInComplete = createAction(types.signInComplete);
|
|
||||||
export const signInError = createAction(types.signInError);
|
|
||||||
|
|
||||||
export const fetchUser = createAction(types.fetchUser);
|
export const fetchUser = createAction(types.fetchUser);
|
||||||
export const fetchUserComplete = createAction(types.fetchUserComplete);
|
export const fetchUserComplete = createAction(types.fetchUserComplete);
|
||||||
export const fecthUserError = createAction(types.fetchUserError);
|
export const fecthUserError = createAction(types.fetchUserError);
|
||||||
|
|
||||||
|
export const updateUserSignedIn = createAction(types.updateUserSignedIn);
|
||||||
|
|
||||||
|
export const isSignedInSelector = state => state[ns].isSignedIn;
|
||||||
|
export const userSelector = state => state[ns].user;
|
||||||
|
|
||||||
export const reducer = handleActions(
|
export const reducer = handleActions(
|
||||||
{
|
{
|
||||||
[types.fetchUserComplete]: (state, { payload }) => ({
|
[types.fetchUserComplete]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
user: payload
|
user: payload
|
||||||
|
}),
|
||||||
|
[types.updateUserSignedIn]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
isSignedIn: payload
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initialState
|
initialState
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import { ajax } from 'rxjs/observable/dom/ajax';
|
|
||||||
import { map } from 'rxjs/operators/map';
|
|
||||||
import { catchError } from 'rxjs/operators/catchError';
|
|
||||||
import { switchMap } from 'rxjs/operators/switchMap';
|
|
||||||
import { ofType } from 'redux-observable';
|
|
||||||
|
|
||||||
import { types, signInComplete, signInError } from './';
|
|
||||||
|
|
||||||
export default function signInEpic(action$, _, { window }) {
|
|
||||||
return action$.pipe(
|
|
||||||
ofType(types.signIn),
|
|
||||||
switchMap(({ payload }) => {
|
|
||||||
const request = {
|
|
||||||
url: 'http://localhost:3000/passwordless-auth',
|
|
||||||
method: 'POST',
|
|
||||||
body: { email: payload, return: window.location.origin }
|
|
||||||
};
|
|
||||||
|
|
||||||
return ajax(request).pipe(
|
|
||||||
map(resp => {
|
|
||||||
console.log('RES', resp);
|
|
||||||
return signInComplete();
|
|
||||||
}),
|
|
||||||
catchError(e => {
|
|
||||||
console.warn(e);
|
|
||||||
return signInError(e);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
@ -11,6 +11,7 @@ import Editor from './Editor';
|
|||||||
import Preview from '../components/Preview';
|
import Preview from '../components/Preview';
|
||||||
import SidePanel from '../components/Side-Panel';
|
import SidePanel from '../components/Side-Panel';
|
||||||
import CompletionModal from '../components/CompletionModal';
|
import CompletionModal from '../components/CompletionModal';
|
||||||
|
import HelpModal from '../components/HelpModal';
|
||||||
|
|
||||||
import { challengeTypes } from '../../../../utils/challengeTypes';
|
import { challengeTypes } from '../../../../utils/challengeTypes';
|
||||||
import { ChallengeNode } from '../../../redux/propTypes';
|
import { ChallengeNode } from '../../../redux/propTypes';
|
||||||
@ -53,12 +54,12 @@ class ShowClassic extends PureComponent {
|
|||||||
createFiles,
|
createFiles,
|
||||||
initTests,
|
initTests,
|
||||||
updateChallengeMeta,
|
updateChallengeMeta,
|
||||||
data: { challengeNode: { files, fields: { tests } } },
|
data: { challengeNode: { files, title, fields: { tests } } },
|
||||||
pathContext: { challengeMeta }
|
pathContext: { challengeMeta }
|
||||||
} = this.props;
|
} = this.props;
|
||||||
createFiles(files);
|
createFiles(files);
|
||||||
initTests(tests);
|
initTests(tests);
|
||||||
updateChallengeMeta(challengeMeta);
|
updateChallengeMeta({ ...challengeMeta, title });
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@ -75,7 +76,7 @@ class ShowClassic extends PureComponent {
|
|||||||
if (prevTitle !== currentTitle) {
|
if (prevTitle !== currentTitle) {
|
||||||
createFiles(files);
|
createFiles(files);
|
||||||
initTests(tests);
|
initTests(tests);
|
||||||
updateChallengeMeta(challengeMeta);
|
updateChallengeMeta({ ...challengeMeta, title: currentTitle });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,6 +134,7 @@ class ShowClassic extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
</ReflexContainer>
|
</ReflexContainer>
|
||||||
<CompletionModal />
|
<CompletionModal />
|
||||||
|
<HelpModal />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Button, Modal } from 'react-bootstrap';
|
import { Button, Modal } from 'react-bootstrap';
|
||||||
|
|
||||||
import ns from './ns.json';
|
import { createQuestion, closeModal, isHelpModalOpenSelector } from '../redux';
|
||||||
import {
|
|
||||||
createQuestion,
|
|
||||||
closeHelpModal,
|
|
||||||
helpModalSelector
|
|
||||||
} from './redux';
|
|
||||||
import { RSA } from '../../../utils/constantStrings.json';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({ isOpen: helpModalSelector(state) });
|
const mapStateToProps = state => ({ isOpen: isHelpModalOpenSelector(state) });
|
||||||
const mapDispatchToProps = { createQuestion, closeHelpModal };
|
const mapDispatchToProps = dispatch =>
|
||||||
|
bindActionCreators(
|
||||||
|
{ createQuestion, closeHelpModal: () => closeModal('help') },
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
closeHelpModal: PropTypes.func.isRequired,
|
closeHelpModal: PropTypes.func.isRequired,
|
||||||
@ -20,46 +19,42 @@ const propTypes = {
|
|||||||
isOpen: PropTypes.bool
|
isOpen: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RSA =
|
||||||
|
'https://forum.freecodecamp.org/t/the-read-search-ask-methodology-for-' +
|
||||||
|
'getting-unstuck/137307';
|
||||||
|
|
||||||
export class HelpModal extends PureComponent {
|
export class HelpModal extends PureComponent {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { isOpen, closeHelpModal, createQuestion } = this.props;
|
||||||
isOpen,
|
|
||||||
closeHelpModal,
|
|
||||||
createQuestion
|
|
||||||
} = this.props;
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal show={isOpen}>
|
||||||
show={ isOpen }
|
<Modal.Header>
|
||||||
>
|
|
||||||
<Modal.Header className={ `${ns}-list-header` }>
|
|
||||||
Ask for help?
|
Ask for help?
|
||||||
<span
|
<span className='close closing-x' onClick={closeHelpModal}>
|
||||||
className='close closing-x'
|
|
||||||
onClick={ closeHelpModal }
|
|
||||||
>
|
|
||||||
×
|
×
|
||||||
</span>
|
</span>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body className='text-center'>
|
<Modal.Body className='text-center'>
|
||||||
<h3 className={`${ns}-help-modal-heading`}>
|
<h3>
|
||||||
If you've already tried the
|
If you've already tried the
|
||||||
<a href={ RSA } target='_blank' title='Read, search, ask'>
|
<a href={RSA} target='_blank' title='Read, search, ask'>
|
||||||
Read-Search-Ask</a> method, then you can ask for help
|
Read-Search-Ask
|
||||||
on the freeCodeCamp forum.
|
</a> method, then you can ask for help on the freeCodeCamp
|
||||||
|
forum.
|
||||||
</h3>
|
</h3>
|
||||||
<Button
|
<Button
|
||||||
block={ true }
|
block={true}
|
||||||
bsSize='lg'
|
bsSize='lg'
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
onClick={ createQuestion }
|
onClick={createQuestion}
|
||||||
>
|
>
|
||||||
Create a help post on the forum
|
Create a help post on the forum
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
block={ true }
|
block={true}
|
||||||
bsSize='lg'
|
bsSize='lg'
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
onClick={ closeHelpModal }
|
onClick={closeHelpModal}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
@ -17,7 +17,9 @@ import {
|
|||||||
consoleOutputSelector,
|
consoleOutputSelector,
|
||||||
challengeTestsSelector,
|
challengeTestsSelector,
|
||||||
executeChallenge,
|
executeChallenge,
|
||||||
initConsole
|
resetChallenge,
|
||||||
|
initConsole,
|
||||||
|
openModal
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
@ -27,14 +29,24 @@ const mapStateToProps = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = dispatch =>
|
||||||
bindActionCreators({ executeChallenge, initConsole }, dispatch);
|
bindActionCreators(
|
||||||
|
{
|
||||||
|
executeChallenge,
|
||||||
|
resetChallenge,
|
||||||
|
initConsole,
|
||||||
|
openHelpModal: () => openModal('help')
|
||||||
|
},
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
description: PropTypes.arrayOf(PropTypes.string),
|
description: PropTypes.arrayOf(PropTypes.string),
|
||||||
executeChallenge: PropTypes.func.isRequired,
|
executeChallenge: PropTypes.func.isRequired,
|
||||||
guideUrl: PropTypes.string,
|
guideUrl: PropTypes.string,
|
||||||
initConsole: PropTypes.func.isRequired,
|
initConsole: PropTypes.func.isRequired,
|
||||||
|
openHelpModal: PropTypes.func.isRequired,
|
||||||
output: PropTypes.string,
|
output: PropTypes.string,
|
||||||
|
resetChallenge: PropTypes.func.isRequired,
|
||||||
tests: PropTypes.arrayOf(
|
tests: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
@ -76,7 +88,9 @@ export class SidePanel extends PureComponent {
|
|||||||
tests = [],
|
tests = [],
|
||||||
output = '',
|
output = '',
|
||||||
guideUrl,
|
guideUrl,
|
||||||
executeChallenge
|
executeChallenge,
|
||||||
|
resetChallenge,
|
||||||
|
openHelpModal
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={'instructions-panel'} ref='panel' role='complementary'>
|
<div className={'instructions-panel'} ref='panel' role='complementary'>
|
||||||
@ -85,7 +99,12 @@ export class SidePanel extends PureComponent {
|
|||||||
<ChallengeTitle>{title}</ChallengeTitle>
|
<ChallengeTitle>{title}</ChallengeTitle>
|
||||||
<ChallengeDescription description={description} />
|
<ChallengeDescription description={description} />
|
||||||
</div>
|
</div>
|
||||||
<ToolPanel executeChallenge={executeChallenge} guideUrl={guideUrl} />
|
<ToolPanel
|
||||||
|
executeChallenge={executeChallenge}
|
||||||
|
guideUrl={guideUrl}
|
||||||
|
openHelpModal={openHelpModal}
|
||||||
|
reset={resetChallenge}
|
||||||
|
/>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<Output
|
<Output
|
||||||
defaultOutput={`/**
|
defaultOutput={`/**
|
||||||
|
@ -5,10 +5,12 @@ import { Button } from 'react-bootstrap';
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
executeChallenge: PropTypes.func.isRequired,
|
executeChallenge: PropTypes.func.isRequired,
|
||||||
guideUrl: PropTypes.string
|
guideUrl: PropTypes.string,
|
||||||
|
openHelpModal: PropTypes.func.isRequired,
|
||||||
|
reset: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
function ToolPanel({ executeChallenge, guideUrl }) {
|
function ToolPanel({ executeChallenge, guideUrl, reset, openHelpModal }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
@ -20,8 +22,13 @@ function ToolPanel({ executeChallenge, guideUrl }) {
|
|||||||
Run tests (Ctrl + Enter)
|
Run tests (Ctrl + Enter)
|
||||||
</Button>
|
</Button>
|
||||||
<div className='button-spacer' />
|
<div className='button-spacer' />
|
||||||
<Button block={true} bsStyle='primary' className='btn-big'>
|
<Button
|
||||||
Reset your code
|
block={true}
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-big'
|
||||||
|
onClick={reset}
|
||||||
|
>
|
||||||
|
Reset this lesson
|
||||||
</Button>
|
</Button>
|
||||||
<div className='button-spacer' />
|
<div className='button-spacer' />
|
||||||
{guideUrl && (
|
{guideUrl && (
|
||||||
@ -38,7 +45,12 @@ function ToolPanel({ executeChallenge, guideUrl }) {
|
|||||||
<div className='button-spacer' />
|
<div className='button-spacer' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button block={true} bsStyle='primary' className='btn-big'>
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-big'
|
||||||
|
onClick={openHelpModal}
|
||||||
|
>
|
||||||
Ask for help on the forum
|
Ask for help on the forum
|
||||||
</Button>
|
</Button>
|
||||||
<div className='button-spacer' />
|
<div className='button-spacer' />
|
||||||
|
@ -2,10 +2,10 @@ import React, { PureComponent } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form
|
||||||
isValidURL,
|
// isValidURL,
|
||||||
makeRequired,
|
// makeRequired,
|
||||||
createFormValidator
|
// createFormValidator
|
||||||
} from '../../../components/formHelpers';
|
} from '../../../components/formHelpers';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
@ -16,14 +16,14 @@ const propTypes = {
|
|||||||
const frontEndFields = ['solution'];
|
const frontEndFields = ['solution'];
|
||||||
const backEndFields = ['solution', 'githubLink'];
|
const backEndFields = ['solution', 'githubLink'];
|
||||||
|
|
||||||
const fieldValidators = {
|
// const fieldValidators = {
|
||||||
solution: makeRequired(isValidURL)
|
// solution: makeRequired(isValidURL)
|
||||||
};
|
// };
|
||||||
|
|
||||||
const backEndFieldValidators = {
|
// const backEndFieldValidators = {
|
||||||
...fieldValidators,
|
// ...fieldValidators,
|
||||||
githubLink: makeRequired(isValidURL)
|
// githubLink: makeRequired(isValidURL)
|
||||||
};
|
// };
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
types: {
|
types: {
|
||||||
@ -50,9 +50,9 @@ export class ProjectForm extends PureComponent {
|
|||||||
id={isFrontEnd ? 'front-end-form' : 'back-end-form'}
|
id={isFrontEnd ? 'front-end-form' : 'back-end-form'}
|
||||||
options={options}
|
options={options}
|
||||||
submit={this.handleSubmit}
|
submit={this.handleSubmit}
|
||||||
validate={createFormValidator(
|
// validate={createFormValidator(
|
||||||
isFrontEnd ? fieldValidators : backEndFieldValidators
|
// isFrontEnd ? fieldValidators : backEndFieldValidators
|
||||||
)}
|
// )}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,58 @@
|
|||||||
/* global graphql */
|
/* global graphql */
|
||||||
import React, { PureComponent, Fragment } from 'react';
|
import React, { PureComponent, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
// import { createSelector } from 'reselect';
|
import { connect } from 'react-redux';
|
||||||
// import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
|
|
||||||
import { ChallengeNode } from '../../../redux/propTypes';
|
import { ChallengeNode } from '../../../redux/propTypes';
|
||||||
import SidePanel from './Side-Panel';
|
import SidePanel from './Side-Panel';
|
||||||
import ToolPanel from './Tool-Panel';
|
import ToolPanel from './Tool-Panel';
|
||||||
// import HelpModal from '../components/Help-Modal.jsx';
|
import HelpModal from '../components/HelpModal';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { updateChallengeMeta, createFiles } from '../redux';
|
||||||
|
|
||||||
|
const mapStateToProps = () => ({});
|
||||||
|
const mapDispatchToProps = dispatch =>
|
||||||
|
bindActionCreators({ updateChallengeMeta, createFiles }, dispatch);
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
createFiles: PropTypes.func.isRequired,
|
||||||
data: PropTypes.shape({
|
data: PropTypes.shape({
|
||||||
challengeNode: ChallengeNode
|
challengeNode: ChallengeNode
|
||||||
})
|
}),
|
||||||
|
pathContext: PropTypes.shape({
|
||||||
|
challengeMeta: PropTypes.object
|
||||||
|
}),
|
||||||
|
updateChallengeMeta: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Project extends PureComponent {
|
export class Project extends PureComponent {
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
createFiles,
|
||||||
|
data: { challengeNode: { title } },
|
||||||
|
pathContext: { challengeMeta },
|
||||||
|
updateChallengeMeta
|
||||||
|
} = this.props;
|
||||||
|
createFiles({});
|
||||||
|
return updateChallengeMeta({ ...challengeMeta, title });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const { data: { challengeNode: { title: prevTitle } } } = prevProps;
|
||||||
|
const {
|
||||||
|
createFiles,
|
||||||
|
data: { challengeNode: { title: currentTitle } },
|
||||||
|
pathContext: { challengeMeta },
|
||||||
|
updateChallengeMeta
|
||||||
|
} = this.props;
|
||||||
|
if (prevTitle !== currentTitle) {
|
||||||
|
createFiles({});
|
||||||
|
updateChallengeMeta({ ...challengeMeta, title: currentTitle });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
@ -40,7 +75,8 @@ export class Project extends PureComponent {
|
|||||||
guideUrl={guideUrl}
|
guideUrl={guideUrl}
|
||||||
title={blockNameTitle}
|
title={blockNameTitle}
|
||||||
/>
|
/>
|
||||||
<ToolPanel challengeType={challengeType} helpChatRoom='help' />
|
<ToolPanel challengeType={challengeType} />
|
||||||
|
<HelpModal />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -49,7 +85,7 @@ export class Project extends PureComponent {
|
|||||||
Project.displayName = 'Project';
|
Project.displayName = 'Project';
|
||||||
Project.propTypes = propTypes;
|
Project.propTypes = propTypes;
|
||||||
|
|
||||||
export default Project;
|
export default connect(mapStateToProps, mapDispatchToProps)(Project);
|
||||||
|
|
||||||
export const query = graphql`
|
export const query = graphql`
|
||||||
query ProjectChallenge($slug: String!) {
|
query ProjectChallenge($slug: String!) {
|
||||||
|
@ -1,55 +1,37 @@
|
|||||||
import React, { PureComponent, Fragment } from 'react';
|
import React, { PureComponent, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
// import { connect } from 'react-redux';
|
import { bindActionCreators } from 'redux';
|
||||||
// import { createSelector } from 'reselect';
|
import { connect } from 'react-redux';
|
||||||
import { Button } from 'react-bootstrap';
|
import { Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
// import { submittingSelector } from './redux';
|
||||||
|
import { openModal } from '../redux';
|
||||||
|
import { frontEndProject } from '../../../../utils/challengeTypes';
|
||||||
|
|
||||||
import ButtonSpacer from '../../../components/util/ButtonSpacer';
|
import ButtonSpacer from '../../../components/util/ButtonSpacer';
|
||||||
import ProjectForm from './ProjectForm';
|
import ProjectForm from './ProjectForm';
|
||||||
|
|
||||||
// import { submittingSelector } from './redux';
|
const mapStateToProps = () => ({});
|
||||||
|
|
||||||
// import {
|
const mapDispatchToProps = dispatch =>
|
||||||
// openChallengeModal,
|
bindActionCreators({ openHelpModal: () => openModal('help') }, dispatch);
|
||||||
|
|
||||||
// openHelpModal,
|
|
||||||
// chatRoomSelector,
|
|
||||||
// guideURLSelector
|
|
||||||
// } from '../../redux';
|
|
||||||
|
|
||||||
// import {
|
|
||||||
// signInLoadingSelector,
|
|
||||||
// challengeSelector
|
|
||||||
// } from '../../../../redux';
|
|
||||||
import { frontEndProject } from '../../../../utils/challengeTypes';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
challengeType: PropTypes.number,
|
challengeType: PropTypes.number,
|
||||||
guideUrl: PropTypes.string,
|
guideUrl: PropTypes.string,
|
||||||
helpChatRoom: PropTypes.string.isRequired
|
openHelpModal: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ToolPanel extends PureComponent {
|
export class ToolPanel extends PureComponent {
|
||||||
render() {
|
render() {
|
||||||
const { guideUrl, helpChatRoom, challengeType } = this.props;
|
const { guideUrl, challengeType, openHelpModal } = this.props;
|
||||||
console.log(challengeType, frontEndProject);
|
console.log(this.props);
|
||||||
|
|
||||||
const isFrontEnd = challengeType === frontEndProject;
|
const isFrontEnd = challengeType === frontEndProject;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ProjectForm isFrontEnd={isFrontEnd} />
|
<ProjectForm isFrontEnd={isFrontEnd} />
|
||||||
<ButtonSpacer />
|
<ButtonSpacer />
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-primary-ghost btn-big'
|
|
||||||
componentClass='a'
|
|
||||||
href={`https://gitter.im/freecodecamp/${helpChatRoom}`}
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
Help
|
|
||||||
</Button>
|
|
||||||
<ButtonSpacer />
|
|
||||||
{guideUrl && (
|
{guideUrl && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Button
|
<Button
|
||||||
@ -68,6 +50,7 @@ export class ToolPanel extends PureComponent {
|
|||||||
block={true}
|
block={true}
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
className='btn-primary-ghost btn-big'
|
className='btn-primary-ghost btn-big'
|
||||||
|
onClick={openHelpModal}
|
||||||
>
|
>
|
||||||
Ask for help on the forum
|
Ask for help on the forum
|
||||||
</Button>
|
</Button>
|
||||||
@ -80,4 +63,4 @@ export class ToolPanel extends PureComponent {
|
|||||||
ToolPanel.displayName = 'ProjectToolPanel';
|
ToolPanel.displayName = 'ProjectToolPanel';
|
||||||
ToolPanel.propTypes = propTypes;
|
ToolPanel.propTypes = propTypes;
|
||||||
|
|
||||||
export default ToolPanel;
|
export default connect(mapStateToProps, mapDispatchToProps)(ToolPanel);
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
// import {
|
|
||||||
// createAction,
|
|
||||||
// createTypes,
|
|
||||||
// handleActions
|
|
||||||
// } from 'berkeleys-redux-utils';
|
|
||||||
// import ns from '../ns.json';
|
|
||||||
|
|
||||||
// export const types = createTypes(['showProjectSubmit'], ns);
|
|
||||||
|
|
||||||
// export const showProjectSubmit = createAction(types.showProjectSubmit);
|
|
||||||
|
|
||||||
// const initialState = {
|
|
||||||
// // project is ready to submit
|
|
||||||
// isSubmitting: false
|
|
||||||
// };
|
|
||||||
// export const submittingSelector = state => state[ns].isSubmitting;
|
|
||||||
|
|
||||||
// export default handleActions(
|
|
||||||
// () => ({
|
|
||||||
// [types.showProjectSubmit]: state => ({
|
|
||||||
// ...state,
|
|
||||||
// isSubmitting: true
|
|
||||||
// })
|
|
||||||
// }),
|
|
||||||
// initialState,
|
|
||||||
// ns
|
|
||||||
// );
|
|
@ -1,11 +0,0 @@
|
|||||||
// import { callIfDefined, formatUrl } from '../../../../../utils/form';
|
|
||||||
|
|
||||||
// export default {
|
|
||||||
// NewFrontEndProject: {
|
|
||||||
// solution: callIfDefined(formatUrl)
|
|
||||||
// },
|
|
||||||
// NewBackEndProject: {
|
|
||||||
// githubLink: callIfDefined(formatUrl),
|
|
||||||
// solution: callIfDefined(formatUrl)
|
|
||||||
// }
|
|
||||||
// };
|
|
@ -0,0 +1,67 @@
|
|||||||
|
import { ofType } from 'redux-observable';
|
||||||
|
import {
|
||||||
|
types,
|
||||||
|
closeModal,
|
||||||
|
challengeFilesSelector,
|
||||||
|
challengeMetaSelector
|
||||||
|
} from '../redux';
|
||||||
|
import { tap, mapTo } from 'rxjs/operators';
|
||||||
|
|
||||||
|
function filesToMarkdown(files = {}) {
|
||||||
|
const moreThenOneFile = Object.keys(files).length > 1;
|
||||||
|
return Object.keys(files).reduce((fileString, key) => {
|
||||||
|
const file = files[key];
|
||||||
|
if (!file) {
|
||||||
|
return fileString;
|
||||||
|
}
|
||||||
|
const fileName = moreThenOneFile ? `\\ file: ${file.contents}` : '';
|
||||||
|
const fileType = file.ext;
|
||||||
|
return (
|
||||||
|
fileString +
|
||||||
|
'```' +
|
||||||
|
fileType +
|
||||||
|
'\n' +
|
||||||
|
fileName +
|
||||||
|
'\n' +
|
||||||
|
file.contents +
|
||||||
|
'\n' +
|
||||||
|
'```\n\n'
|
||||||
|
);
|
||||||
|
}, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createQuestionEpic(action$, { getState }, { window }) {
|
||||||
|
return action$.pipe(
|
||||||
|
ofType(types.createQuestion),
|
||||||
|
tap(() => {
|
||||||
|
const state = getState();
|
||||||
|
const files = challengeFilesSelector(state);
|
||||||
|
const { title: challengeTitle } = challengeMetaSelector(state);
|
||||||
|
const { navigator: { userAgent }, location: { href } } = window;
|
||||||
|
const textMessage = [
|
||||||
|
"**Tell us what's happening:**\n\n\n\n",
|
||||||
|
'**Your code so far**\n',
|
||||||
|
filesToMarkdown(files),
|
||||||
|
'**Your browser information:**\n\n',
|
||||||
|
'User Agent is: <code>',
|
||||||
|
userAgent,
|
||||||
|
'</code>.\n\n',
|
||||||
|
'**Link to the challenge:**\n',
|
||||||
|
href
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
'https://forum.freecodecamp.org/new-topic' +
|
||||||
|
'?category=help' +
|
||||||
|
'&title=' +
|
||||||
|
window.encodeURIComponent(challengeTitle) +
|
||||||
|
'&body=' +
|
||||||
|
window.encodeURIComponent(textMessage),
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
mapTo(closeModal('help'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createQuestionEpic;
|
@ -6,6 +6,7 @@ import challengeModalEpic from './challenge-modal-epic';
|
|||||||
import completionEpic from './completion-epic';
|
import completionEpic from './completion-epic';
|
||||||
import executeChallengeEpic from './execute-challenge-epic';
|
import executeChallengeEpic from './execute-challenge-epic';
|
||||||
import codeLockEpic from './code-lock-epic';
|
import codeLockEpic from './code-lock-epic';
|
||||||
|
import createQuestionEpic from './create-question-epic';
|
||||||
|
|
||||||
const ns = 'challenge';
|
const ns = 'challenge';
|
||||||
export const backendNS = 'backendChallenge';
|
export const backendNS = 'backendChallenge';
|
||||||
@ -30,12 +31,14 @@ export const epics = [
|
|||||||
challengeModalEpic,
|
challengeModalEpic,
|
||||||
codeLockEpic,
|
codeLockEpic,
|
||||||
completionEpic,
|
completionEpic,
|
||||||
|
createQuestionEpic,
|
||||||
executeChallengeEpic
|
executeChallengeEpic
|
||||||
];
|
];
|
||||||
|
|
||||||
export const types = createTypes(
|
export const types = createTypes(
|
||||||
[
|
[
|
||||||
'createFiles',
|
'createFiles',
|
||||||
|
'createQuestion',
|
||||||
'initTests',
|
'initTests',
|
||||||
'initConsole',
|
'initConsole',
|
||||||
'updateConsole',
|
'updateConsole',
|
||||||
@ -53,6 +56,7 @@ export const types = createTypes(
|
|||||||
|
|
||||||
'checkChallenge',
|
'checkChallenge',
|
||||||
'executeChallenge',
|
'executeChallenge',
|
||||||
|
'resetChallenge',
|
||||||
'submitChallenge'
|
'submitChallenge'
|
||||||
],
|
],
|
||||||
ns
|
ns
|
||||||
@ -73,6 +77,7 @@ export const createFiles = createAction(types.createFiles, challengeFiles =>
|
|||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
export const createQuestion = createAction(types.createQuestion);
|
||||||
export const initTests = createAction(types.initTests);
|
export const initTests = createAction(types.initTests);
|
||||||
export const updateTests = createAction(types.updateTests);
|
export const updateTests = createAction(types.updateTests);
|
||||||
|
|
||||||
@ -94,6 +99,7 @@ export const openModal = createAction(types.openModal);
|
|||||||
|
|
||||||
export const checkChallenge = createAction(types.checkChallenge);
|
export const checkChallenge = createAction(types.checkChallenge);
|
||||||
export const executeChallenge = createAction(types.executeChallenge);
|
export const executeChallenge = createAction(types.executeChallenge);
|
||||||
|
export const resetChallenge = createAction(types.resetChallenge);
|
||||||
export const submitChallenge = createAction(types.submitChallenge);
|
export const submitChallenge = createAction(types.submitChallenge);
|
||||||
|
|
||||||
export const backendFormValuesSelector = state => state.form[backendNS];
|
export const backendFormValuesSelector = state => state.form[backendNS];
|
||||||
@ -103,6 +109,7 @@ export const challengeTestsSelector = state => state[ns].challengeTests;
|
|||||||
export const consoleOutputSelector = state => state[ns].consoleOut;
|
export const consoleOutputSelector = state => state[ns].consoleOut;
|
||||||
export const isCompletionModalOpenSelector = state =>
|
export const isCompletionModalOpenSelector = state =>
|
||||||
state[ns].modal.completion;
|
state[ns].modal.completion;
|
||||||
|
export const isHelpModalOpenSelector = state => state[ns].modal.help;
|
||||||
export const isJSEnabledSelector = state => state[ns].isJSEnabled;
|
export const isJSEnabledSelector = state => state[ns].isJSEnabled;
|
||||||
export const successMessageSelector = state => state[ns].successMessage;
|
export const successMessageSelector = state => state[ns].successMessage;
|
||||||
|
|
||||||
@ -112,6 +119,16 @@ export const reducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
challengeFiles: payload
|
challengeFiles: payload
|
||||||
}),
|
}),
|
||||||
|
[types.updateFile]: (state, { payload: { key, editorValue } }) => ({
|
||||||
|
...state,
|
||||||
|
challengeFiles: {
|
||||||
|
...state.challengeFiles,
|
||||||
|
[key]: {
|
||||||
|
...state.challengeFiles[key],
|
||||||
|
contents: editorValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
[types.initTests]: (state, { payload }) => ({
|
[types.initTests]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
challengeTests: payload
|
challengeTests: payload
|
||||||
@ -124,24 +141,38 @@ export const reducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
consoleOut: payload
|
consoleOut: payload
|
||||||
}),
|
}),
|
||||||
[types.updateChallengeMeta]: (state, { payload }) => ({
|
|
||||||
...state,
|
|
||||||
challengeMeta: { ...payload }
|
|
||||||
}),
|
|
||||||
[types.updateConsole]: (state, { payload }) => ({
|
[types.updateConsole]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
consoleOut: state.consoleOut + '\n' + payload
|
consoleOut: state.consoleOut + '\n' + payload
|
||||||
}),
|
}),
|
||||||
[types.updateFile]: (state, { payload: { key, editorValue } }) => ({
|
|
||||||
|
[types.updateChallengeMeta]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
challengeMeta: { ...payload }
|
||||||
|
}),
|
||||||
|
|
||||||
|
[types.resetChallenge]: state => ({
|
||||||
...state,
|
...state,
|
||||||
challengeFiles: {
|
challengeFiles: {
|
||||||
...state.challengeFiles,
|
...Object.keys(state.challengeFiles)
|
||||||
[key]: {
|
.map(key => state.challengeFiles[key])
|
||||||
...state.challengeFiles[key],
|
.reduce(
|
||||||
contents: editorValue
|
(files, file) => ({
|
||||||
}
|
...files,
|
||||||
|
[file.key]: {
|
||||||
|
...file,
|
||||||
|
contents: file.seed.slice()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
challengeTests: state.challengeTests.map(({ text, testString }) => ({
|
||||||
|
text,
|
||||||
|
testString
|
||||||
|
})),
|
||||||
|
consoleOut: ''
|
||||||
|
}),
|
||||||
[types.unlockCode]: state => ({
|
[types.unlockCode]: state => ({
|
||||||
...state,
|
...state,
|
||||||
isJSEnabled: true
|
isJSEnabled: true
|
||||||
|
@ -523,6 +523,18 @@ atob@^2.0.0:
|
|||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.0.tgz#ab2b150e51d7b122b9efc8d7340c06b6c41076bc"
|
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.0.tgz#ab2b150e51d7b122b9efc8d7340c06b6c41076bc"
|
||||||
|
|
||||||
|
auth0-js@^9.5.1:
|
||||||
|
version "9.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/auth0-js/-/auth0-js-9.5.1.tgz#34dea6b0f11b5e5ee139605611f49b1c0f15dbb1"
|
||||||
|
dependencies:
|
||||||
|
base64-js "^1.2.0"
|
||||||
|
idtoken-verifier "^1.2.0"
|
||||||
|
js-cookie "^2.2.0"
|
||||||
|
qs "^6.4.0"
|
||||||
|
superagent "^3.8.2"
|
||||||
|
url-join "^1.1.0"
|
||||||
|
winchan "^0.2.0"
|
||||||
|
|
||||||
autoprefixer@^6.0.2, autoprefixer@^6.3.1:
|
autoprefixer@^6.0.2, autoprefixer@^6.3.1:
|
||||||
version "6.7.7"
|
version "6.7.7"
|
||||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014"
|
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014"
|
||||||
@ -1448,6 +1460,10 @@ base64-js@^1.0.2:
|
|||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.3.tgz#fb13668233d9614cf5fb4bce95a9ba4096cdf801"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.3.tgz#fb13668233d9614cf5fb4bce95a9ba4096cdf801"
|
||||||
|
|
||||||
|
base64-js@^1.2.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
|
||||||
|
|
||||||
base64id@1.0.0:
|
base64id@1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
|
resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
|
||||||
@ -2260,7 +2276,7 @@ component-emitter@1.2.0:
|
|||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.0.tgz#ccd113a86388d06482d03de3fc7df98526ba8efe"
|
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.0.tgz#ccd113a86388d06482d03de3fc7df98526ba8efe"
|
||||||
|
|
||||||
component-emitter@1.2.1, component-emitter@^1.2.1:
|
component-emitter@1.2.1, component-emitter@^1.2.0, component-emitter@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
|
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
|
||||||
|
|
||||||
@ -2356,6 +2372,10 @@ cookie@0.3.1:
|
|||||||
version "0.3.1"
|
version "0.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
|
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
|
||||||
|
|
||||||
|
cookiejar@^2.1.0:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a"
|
||||||
|
|
||||||
copy-concurrently@^1.0.0:
|
copy-concurrently@^1.0.0:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
|
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
|
||||||
@ -2508,6 +2528,10 @@ crypto-browserify@^3.11.0:
|
|||||||
randombytes "^2.0.0"
|
randombytes "^2.0.0"
|
||||||
randomfill "^1.0.3"
|
randomfill "^1.0.3"
|
||||||
|
|
||||||
|
crypto-js@^3.1.9-1:
|
||||||
|
version "3.1.9-1"
|
||||||
|
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.1.9-1.tgz#fda19e761fc077e01ffbfdc6e9fdfc59e8806cd8"
|
||||||
|
|
||||||
crypto-random-string@^1.0.0:
|
crypto-random-string@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
|
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
|
||||||
@ -3891,6 +3915,14 @@ forever-agent@~0.6.1:
|
|||||||
version "0.6.1"
|
version "0.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||||
|
|
||||||
|
form-data@^2.3.1, form-data@~2.3.1:
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
|
||||||
|
dependencies:
|
||||||
|
asynckit "^0.4.0"
|
||||||
|
combined-stream "1.0.6"
|
||||||
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
form-data@~2.1.1:
|
form-data@~2.1.1:
|
||||||
version "2.1.4"
|
version "2.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
|
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
|
||||||
@ -3899,13 +3931,9 @@ form-data@~2.1.1:
|
|||||||
combined-stream "^1.0.5"
|
combined-stream "^1.0.5"
|
||||||
mime-types "^2.1.12"
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
form-data@~2.3.1:
|
formidable@^1.2.0:
|
||||||
version "2.3.2"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
|
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659"
|
||||||
dependencies:
|
|
||||||
asynckit "^0.4.0"
|
|
||||||
combined-stream "1.0.6"
|
|
||||||
mime-types "^2.1.12"
|
|
||||||
|
|
||||||
forwarded@~0.1.2:
|
forwarded@~0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
@ -4969,6 +4997,16 @@ icss-replace-symbols@^1.1.0:
|
|||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
|
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
|
||||||
|
|
||||||
|
idtoken-verifier@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/idtoken-verifier/-/idtoken-verifier-1.2.0.tgz#4654f1f07ab7a803fc9b1b8b36057e2a87ad8b09"
|
||||||
|
dependencies:
|
||||||
|
base64-js "^1.2.0"
|
||||||
|
crypto-js "^3.1.9-1"
|
||||||
|
jsbn "^0.1.0"
|
||||||
|
superagent "^3.8.2"
|
||||||
|
url-join "^1.1.0"
|
||||||
|
|
||||||
ieee754@^1.1.4:
|
ieee754@^1.1.4:
|
||||||
version "1.1.11"
|
version "1.1.11"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455"
|
||||||
@ -5868,6 +5906,10 @@ js-base64@^2.1.9:
|
|||||||
version "2.4.3"
|
version "2.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582"
|
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582"
|
||||||
|
|
||||||
|
js-cookie@^2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.0.tgz#1b2c279a6eece380a12168b92485265b35b1effb"
|
||||||
|
|
||||||
js-tokens@^3.0.0, js-tokens@^3.0.2:
|
js-tokens@^3.0.0, js-tokens@^3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||||
@ -5890,7 +5932,7 @@ jsan@^3.1.5, jsan@^3.1.9:
|
|||||||
version "3.1.9"
|
version "3.1.9"
|
||||||
resolved "https://registry.yarnpkg.com/jsan/-/jsan-3.1.9.tgz#2705676c1058f0a7d9ac266ad036a5769cfa7c96"
|
resolved "https://registry.yarnpkg.com/jsan/-/jsan-3.1.9.tgz#2705676c1058f0a7d9ac266ad036a5769cfa7c96"
|
||||||
|
|
||||||
jsbn@~0.1.0:
|
jsbn@^0.1.0, jsbn@~0.1.0:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||||
|
|
||||||
@ -6582,7 +6624,7 @@ merge@^1.1.3, merge@^1.2.0:
|
|||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
|
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
|
||||||
|
|
||||||
methods@~1.1.2:
|
methods@^1.1.1, methods@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||||
|
|
||||||
@ -6662,7 +6704,7 @@ mime@1.4.1:
|
|||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
|
||||||
|
|
||||||
mime@^1.3.6, mime@^1.5.0:
|
mime@^1.3.6, mime@^1.4.1, mime@^1.5.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||||
|
|
||||||
@ -8298,6 +8340,10 @@ qs@6.5.1, qs@~6.5.1:
|
|||||||
version "6.5.1"
|
version "6.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
|
||||||
|
|
||||||
|
qs@^6.4.0, qs@^6.5.1:
|
||||||
|
version "6.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||||
|
|
||||||
qs@~6.4.0:
|
qs@~6.4.0:
|
||||||
version "6.4.0"
|
version "6.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
|
||||||
@ -10143,6 +10189,21 @@ style-loader@^0.13.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
loader-utils "^1.0.2"
|
loader-utils "^1.0.2"
|
||||||
|
|
||||||
|
superagent@^3.8.2:
|
||||||
|
version "3.8.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128"
|
||||||
|
dependencies:
|
||||||
|
component-emitter "^1.2.0"
|
||||||
|
cookiejar "^2.1.0"
|
||||||
|
debug "^3.1.0"
|
||||||
|
extend "^3.0.0"
|
||||||
|
form-data "^2.3.1"
|
||||||
|
formidable "^1.2.0"
|
||||||
|
methods "^1.1.1"
|
||||||
|
mime "^1.4.1"
|
||||||
|
qs "^6.5.1"
|
||||||
|
readable-stream "^2.3.5"
|
||||||
|
|
||||||
supports-color@^2.0.0:
|
supports-color@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
|
||||||
@ -11157,6 +11218,10 @@ widest-line@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
string-width "^2.1.1"
|
string-width "^2.1.1"
|
||||||
|
|
||||||
|
winchan@^0.2.0:
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/winchan/-/winchan-0.2.0.tgz#3863028e7f974b0da1412f28417ba424972abd94"
|
||||||
|
|
||||||
window-size@0.1.0:
|
window-size@0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
|
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
|
||||||
|
Reference in New Issue
Block a user