diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8af4c5b33c4..cd7cb1341c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -190,11 +190,10 @@ Be sure to post in the PR conversation that you have made the requested changes. ## Other resources -- [Searching for Your Issue on GitHub](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Searching-for-Your-Issue-on-GitHub) +- [Searching for Your Issue on GitHub](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Search-Existing-Issues) - [Creating a New GitHub Issue](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Creating-a-New-GitHub-Issue) -- [Select Issues for Contributing Using Labels](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Select-Issues-for-Contributing-Using-Labels) -- [How to clone the FreeCodeCamp website on a Windows pc](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/How-to-clone-the-FreeCodeCamp-website-on-a-Windows-pc) -- [How to log in to your local FCC site - using GitHub](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/How-To-Log-In-To-Your-Local-FCC-Site) -- [Contributions Guide - With a demo on fixing a typo](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Contributions-Guide---with-Typo-Demo) -- [Writing great git commit message](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Writing-great-git-commit-message) +- [Select Issues for Contributing Using Labels](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/FreeCodeCamp-Issue-Labels) +- [How to clone the FreeCodeCamp website on a Windows pc](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/FreeCodeCamp-Fork-Windows) +- [How to log in to your local FCC site - using GitHub](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/FreeCodeCamp-Log-In-To-Local-Instance) +- [Writing great git commit message](https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/Git-Commit-Message) - [Contributor Chat Support - For the FCC Repositories, and running a local instance] (https://gitter.im/FreeCodeCamp/HelpContributors) diff --git a/client/commonFramework/bindings.js b/client/commonFramework/bindings.js index 1a6178c55d6..4180583f5d5 100644 --- a/client/commonFramework/bindings.js +++ b/client/commonFramework/bindings.js @@ -150,6 +150,11 @@ window.common = (function(global) { window.ga('send', 'event', 'Challenge', 'load', common.gaName); } + $('.modal').on('show.bs.modal', function() { + $('.gitter-chat-embed, .wiki-aside, .map-aside') + .addClass('is-collapsed'); + }); + $('#complete-courseware-dialog').on('hidden.bs.modal', function() { if (common.editor.focus) { common.editor.focus(); @@ -176,6 +181,15 @@ window.common = (function(global) { $('#complete-courseware-dialog').modal('show'); }); + $('#show-solution').on('click', function() { + $('#complete-courseware-dialog').modal('hide'); + $('#nav-wiki-btn').click(); + }); + + $('#challenge-help-btn').on('click', function() { + $('.wiki-aside, .map-aside, #chat-embed-main').addClass('is-collapsed'); + }); + $('#help-ive-found-a-bug-wiki-article').on('click', function() { window.open( 'https://github.com/FreeCodeCamp/FreeCodeCamp/wiki/' + diff --git a/client/commonFramework/output-display.js b/client/commonFramework/output-display.js index 9f96908a1f0..291e61d7f16 100644 --- a/client/commonFramework/output-display.js +++ b/client/commonFramework/output-display.js @@ -39,6 +39,9 @@ window.common = (function(global) { codeOutput.setSize('100%', '100%'); common.updateOutputDisplay = function updateOutputDisplay(str = '') { + if (typeof str === 'function') { + str = str.toString(); + } if (typeof str !== 'string') { str = JSON.stringify(str); } diff --git a/client/less/main.less b/client/less/main.less index 98d2a1846f8..a42153283b3 100644 --- a/client/less/main.less +++ b/client/less/main.less @@ -1155,6 +1155,12 @@ code { } } +// make btn-default hover more visible +.btn-default:active, +.btn-default:hover { + background-color: @brand-primary; +} + @import "chat.less"; @import "jobs.less"; @import "challenge.less"; diff --git a/client/less/wiki.less b/client/less/wiki.less index b5731a84ef5..6a454032a93 100644 --- a/client/less/wiki.less +++ b/client/less/wiki.less @@ -90,10 +90,8 @@ } .wiki-fixed-header { - position: fixed; - background: white; - padding-top: 5px; - width: 100%; + background: linear-gradient(180deg,#fff 0,#fff 70%,hsla(0,0%,100%,0)); + padding: 5px 0 18px 0; z-index: 1; left: 0; top: 0; diff --git a/common/app/routes/Hikes/components/Lecture.jsx b/common/app/routes/Hikes/components/Lecture.jsx index 42dd83a7759..ca7601ba635 100644 --- a/common/app/routes/Hikes/components/Lecture.jsx +++ b/common/app/routes/Hikes/components/Lecture.jsx @@ -76,7 +76,6 @@ export class Lecture extends React.Component { diff --git a/common/models/user.js b/common/models/user.js index 4d2def23047..cb3ec80932e 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -4,12 +4,14 @@ import moment from 'moment'; import dedent from 'dedent'; import debugFactory from 'debug'; import { isEmail } from 'validator'; +import path from 'path'; import { saveUser, observeMethod } from '../../server/utils/rx'; import { blacklistedUsernames } from '../../server/utils/constants'; const debug = debugFactory('fcc:user:remote'); const BROWNIEPOINTS_TIMEOUT = [1, 'hour']; +const isDev = process.env.NODE_ENV !== 'production'; function getAboutProfile({ username, @@ -89,7 +91,7 @@ module.exports = function(User) { 'You\'re email has been confirmed!' ] }); - ctx.res.redirect('/email-signin'); + ctx.res.redirect('/'); }); User.beforeRemote('create', function({ req, res }, _, next) { @@ -323,24 +325,90 @@ module.exports = function(User) { ); User.prototype.updateEmail = function updateEmail(email) { + const fiveMinutesAgo = moment().subtract(5, 'minutes'); + const lastEmailSentAt = moment(new Date(this.emailVerifyTTL || null)); + const ownEmail = email === this.email; + const isWaitPeriodOver = this.emailVerifyTTL ? + lastEmailSentAt.isBefore(fiveMinutesAgo) : + true; + if (!isEmail(email)) { return Promise.reject( - new Error('The submitted email not valid') + new Error('The submitted email not valid.') ); } - if (this.email && this.email === email) { + // email is already associated and verified with this account + if (ownEmail && this.emailVerified) { return Promise.reject(new Error( `${email} is already associated with this account.` )); } + + if (ownEmail && !isWaitPeriodOver) { + const minutesLeft = 5 - + (moment().minutes() - lastEmailSentAt.minutes()); + + const timeToWait = minutesLeft ? + `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` : + 'a few seconds'; + + return Promise.reject(new Error( + `Please wait ${timeToWait} to resend email verification.` + )); + } + return User.doesExist(null, email) .then(exists => { - if (exists) { + // not associated with this account, but is associated with another + if (!ownEmail && exists) { return Promise.reject( new Error(`${email} is already associated with another account.`) ); } - return this.update$({ email }).toPromise(); + + const emailVerified = false; + return this.update$({ + email, + emailVerified, + emailVerifyTTL: new Date() + }) + .do(() => { + this.email = email; + this.emailVerified = emailVerified; + this.emailVerifyTTL = new Date(); + }) + .flatMap(() => { + var mailOptions = { + type: 'email', + to: email, + from: 'Team@freecodecamp.com', + subject: 'Welcome to Free Code Camp!', + protocol: isDev ? null : 'https', + host: isDev ? 'localhost' : 'freecodecamp.com', + port: isDev ? null : 443, + template: path.join( + __dirname, + '..', + '..', + 'server', + 'views', + 'emails', + 'user-email-verify.ejs' + ) + }; + return this.verify(mailOptions); + }) + .map(() => dedent` + Please check your email. + We sent you a link that you can click to verify your email address. + `) + .catch(error => { + debug(error); + return Observable.throw( + 'Oops, something went wrong, please try again later.' + ); + }) + .toPromise(); }); }; @@ -358,8 +426,8 @@ module.exports = function(User) { ], returns: [ { - arg: 'status', - type: 'object' + arg: 'message', + type: 'string' } ], http: { @@ -486,52 +554,11 @@ module.exports = function(User) { } ); - User.prototype.updateEmail = function updateEmail(email) { - if (this.email && this.email === email) { - return Promise.reject(new Error( - `${email} is already associated with this account.` - )); - } - return User.doesExist(null, email) - .then(exists => { - if (exists) { - return Promise.reject( - new Error(`${email} is already associated with another account.`) - ); - } - return this.update$({ email }).toPromise(); - }); - }; - - User.remoteMethod( - 'updateEmail', - { - isStatic: false, - description: 'updates the email of the user object', - accepts: [ - { - arg: 'email', - type: 'string', - required: true - } - ], - returns: [ - { - arg: 'status', - type: 'object' - } - ], - http: { - path: '/update-email', - verb: 'POST' - } - } - ); - User.themes = { night: true, default: true }; + User.prototype.updateTheme = function updateTheme(theme) { if (!this.constructor.themes[theme]) { const err = new Error( diff --git a/common/models/user.json b/common/models/user.json index cdbff24c584..225f58a78c4 100644 --- a/common/models/user.json +++ b/common/models/user.json @@ -16,6 +16,9 @@ } } }, + "emailVerifyTTL": { + "type": "date" + }, "password": { "type": "string" }, diff --git a/public/js/lib/loop-protect/loop-protect.js b/public/js/lib/loop-protect/loop-protect.js index 60031599f54..b4f2fdb454c 100644 --- a/public/js/lib/loop-protect/loop-protect.js +++ b/public/js/lib/loop-protect/loop-protect.js @@ -130,6 +130,7 @@ if (typeof DEBUG === 'undefined') { DEBUG = true; } var ignore = {}; var pushonly = {}; var labelPostion = null; + var labelIndex = -1; function insertReset(lineNum, line, matchPosition) { // recompile the line with the reset **just** before the actual loop @@ -179,6 +180,7 @@ if (typeof DEBUG === 'undefined') { DEBUG = true; } if (directlyBeforeLoop(index, lineNum, lines)) { DEBUG && debug('- found a label: "' + labelMatch[0] + '"'); // jshint ignore:line labelPostion = lineNum; + labelIndex = index; } else { DEBUG && debug('- ignored "label", false positive'); // jshint ignore:line } @@ -309,10 +311,11 @@ if (typeof DEBUG === 'undefined') { DEBUG = true; } DEBUG && debug('- reset inserted above matched label on line ' + labelPostion); // jshint ignore:line if (recompiled[labelPostion] === undefined) { labelPostion--; - matchPosition = 0; + labelIndex = 0; } - recompiled[labelPostion] = insertReset(printLineNumber, recompiled[labelPostion], matchPosition); + recompiled[labelPostion] = insertReset(printLineNumber, recompiled[labelPostion], labelIndex); labelPostion = null; + labelIndex = -1; } } diff --git a/seed/challenges/01-front-end-development-certification/advanced-bonfires.json b/seed/challenges/01-front-end-development-certification/advanced-bonfires.json index f3cb326de9d..d4dfdf0928a 100644 --- a/seed/challenges/01-front-end-development-certification/advanced-bonfires.json +++ b/seed/challenges/01-front-end-development-certification/advanced-bonfires.json @@ -269,7 +269,7 @@ "Convert a date range consisting of two dates formatted as YYYY-MM-DD into a more readable format.", "The friendly display should use month names instead of numbers and ordinal dates instead of cardinal (1st instead of 1).", "Do not display information that is redundant or that can be inferred by the user: if the date range ends in less than a year from when it begins, do not display the ending year.", - "Additionally, if the date range begins in the current year and ends within one year, the year should not be displayed at the beginning of the friendly range.", + "Additionally, if the date range begins in the current year (i.e. it is currently the year 2016) and ends within one year, the year should not be displayed at the beginning of the friendly range.", "If the range ends in the same month that it begins, do not display the ending year or month.", "Examples:", "makeFriendlyDates([\"2016-07-01\", \"2016-07-04\"]) should return [\"July 1st\",\"4th\"]", diff --git a/seed/challenges/01-front-end-development-certification/basic-bonfires.json b/seed/challenges/01-front-end-development-certification/basic-bonfires.json index 5e1bcee4314..7eb7cc57728 100644 --- a/seed/challenges/01-front-end-development-certification/basic-bonfires.json +++ b/seed/challenges/01-front-end-development-certification/basic-bonfires.json @@ -362,7 +362,7 @@ "id": "afcc8d540bea9ea2669306b6", "title": "Repeat a string repeat a string", "description": [ - "Repeat a given string (first argument) num times (second argument). Return an empty string if num is a negative number.", + "Repeat a given string (first argument) num times (second argument). Return an empty string if num is not a positive number.", "Remember to use Read-Search-Ask if you get stuck. Write your own code." ], "challengeSeed": [ diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index 7ac5e11aca8..94fab3ae137 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -643,7 +643,7 @@ ], "tests": [ "assert(remainder === 2, 'message: The value of remainder should be 2');", - "assert(/\\d+\\s*%\\s*\\d+/.test(code), 'message: You should use the % operator');" + "assert(/var\\s*?remainder\\s*?=\\s*?.*%.*;/.test(code), 'message: You should use the % operator');" ], "type": "waypoint", "challengeType": 1, @@ -951,7 +951,7 @@ "Alan said, \"Peter is learning JavaScript\".", "

Instructions

", "Use backslashes to assign a string to the myStr variable so that if you were to print it to the console, you would see:", - "I am a \"double quoted\" string inside \"double quotes\"" + "I am a \"double quoted\" string inside \"double quotes\"." ], "releasedOn": "January 1, 2016", "challengeSeed": [ @@ -969,11 +969,11 @@ "})();" ], "solutions": [ - "var myStr = \"I am a \\\"double quoted\\\" string inside \\\"double quotes\\\"\";" + "var myStr = \"I am a \\\"double quoted\\\" string inside \\\"double quotes\\\".\";" ], "tests": [ - "assert(code.match(/\\\\\"/g).length === 4 && code.match(/[^\\\\]\"/g).length === 2, 'message: You should use two double quotes (") and four escaped double quotes (\") ');", - "assert(myStr === \"I am a \\\"double quoted\\\" string inside \\\"double quotes\\\"\", 'message: Variable myStr should equal to (I am a \"double quoted\" string inside \"double quotes\").');" + "assert(code.match(/\\\\\"/g).length === 4 && code.match(/[^\\\\]\"/g).length === 2, 'message: You should use two double quotes (") and four escaped double quotes (\").');", + "assert(myStr === \"I am a \\\"double quoted\\\" string inside \\\"double quotes\\\".\", 'message: Variable myStr should equal to (I am a \"double quoted\" string inside \"double quotes\".).');" ], "type": "waypoint", "challengeType": 1, @@ -986,7 +986,7 @@ "Alan dijo, \"Pedro está aprendiendo JavaScript\".", "

Instructiones

", "Usa barras invertidaspara asigar una cadena a la variable myStr de modo que si tu fueras a imprimirla en la consola, tu verías:", - "I am a \"double quoted\" string inside \"double quotes\"" + "I am a \"double quoted\" string inside \"double quotes\"." ] }, { @@ -1756,9 +1756,9 @@ "id": "56592a60ddddeae28f7aa8e1", "title": "Access Multi-Dimensional Arrays With Indexes", "description": [ - "One way to think of a multi-dimensional array, is as an array of arrays. When you use brackets to access your array, the first set of bracket refers to the entries in the outer-most array, and each subsequent level of brackets refers to the next level of entries inside.", + "One way to think of a multi-dimensional array, is as an array of arrays. When you use brackets to access your array, the first set of bracket refers to the entries in the outer-most (the first level) array, and each additional pair of brackets refers to the next level of entries inside.", "Example", - "
var arr = [
[1,2,3],
[4,5,6],
[7,8,9],
[[10,11,12], 13, 14]
];
arr[0]; // equals [1,2,3]
arr[1][2]; // equals 6
arr[3][0][1]; // equals 11
", + "
var arr = [
[1,2,3],
[4,5,6],
[7,8,9],
[[10,11,12], 13, 14]
];
arr[3]; // equals [[10,11,12], 13, 14]
arr[3][0]; // equals [10,11,12]
arr[3][0][1]; // equals 11
", "

Instructions

", "Using bracket notation select an element from myArray such that myData is equal to 8." ], @@ -4002,7 +4002,8 @@ "assert(typeof playerNumber === 'number', 'message: playerNumber should be a number');", "assert(typeof player === 'string', 'message: The variable player should be a string');", "assert(player === 'Montana', 'message: The value of player should be \"Montana\"');", - "assert(/testObj\\s*?\\[\\s*playerNumber\\s*\\]/.test(code),'message: You should use bracket notation to access testObj');" + "assert(/testObj\\s*?\\[.*?\\]/.test(code),'message: You should use bracket notation to access testObj');", + "assert(/testObj\\s*?\\[\\s*playerNumber\\s*\\]/.test(code),'message: You should be using the variable playerNumber in your bracket notation');" ], "type": "waypoint", "challengeType": 1, @@ -4550,10 +4551,11 @@ "(function(x) { return \"collection = \\n\" + JSON.stringify(x, '\\n', 2); })(collection);" ], "solutions": [ - "var collection = {\n 2548: {\n album: \"Slippery When Wet\",\n artist: \"Bon Jovi\",\n tracks: [ \n \"Let It Rock\", \n \"You Give Love a Bad Name\" \n ]\n },\n 2468: {\n album: \"1999\",\n artist: \"Prince\",\n tracks: [ \n \"1999\", \n \"Little Red Corvette\" \n ]\n },\n 1245: {\n artist: \"Robert Palmer\",\n tracks: [ ]\n },\n 5439: {\n album: \"ABBA Gold\"\n }\n};\n// Keep a copy of the collection for tests\nvar collectionCopy = JSON.parse(JSON.stringify(collection));\n\n// Only change code below this line\nfunction updateRecords(id, prop, value) {\n if(value !== \"\") {\n if(prop === \"tracks\") {\n collection[id][prop].push(value);\n } else {\n collection[id][prop] = value;\n }\n } else {\n delete collection[id][prop];\n }\n\n return collection;\n}" + "var collection = {\n 2548: {\n album: \"Slippery When Wet\",\n artist: \"Bon Jovi\",\n tracks: [ \n \"Let It Rock\", \n \"You Give Love a Bad Name\" \n ]\n },\n 2468: {\n album: \"1999\",\n artist: \"Prince\",\n tracks: [ \n \"1999\", \n \"Little Red Corvette\" \n ]\n },\n 1245: {\n artist: \"Robert Palmer\",\n tracks: [ ]\n },\n 5439: {\n album: \"ABBA Gold\"\n }\n};\n// Keep a copy of the collection for tests\nvar collectionCopy = JSON.parse(JSON.stringify(collection));\n\n// Only change code below this line\nfunction updateRecords(id, prop, value) {\n if(value !== \"\") {\n if(prop === \"tracks\") {\n collection[id][prop]= collection[id][prop] || [];\n collection[id][prop].push(value);\n } else {\n collection[id][prop] = value;\n }\n } else {\n delete collection[id][prop];\n }\n\n return collection;\n}" ], "tests": [ "collection = collectionCopy; assert(updateRecords(5439, \"artist\", \"ABBA\")[5439][\"artist\"] === \"ABBA\", 'message: After updateRecords(5439, \"artist\", \"ABBA\"), artist should be \"ABBA\"');", + "assert(updateRecords(5439, \"tracks\", \"Take a Chance on Me\")[5439][\"tracks\"].pop() === \"Take a Chance on Me\", 'message: After updateRecords(5439, \"tracks\", \"Take a Chance on Me\"), tracks should have \"Take a Chance on Me\" as the last element.');", "updateRecords(2548, \"artist\", \"\"); assert(!collection[2548].hasOwnProperty(\"artist\"), 'message: After updateRecords(2548, \"artist\", \"\"), artist should not be set');", "assert(updateRecords(1245, \"tracks\", \"Addicted to Love\")[1245][\"tracks\"].pop() === \"Addicted to Love\", 'message: After updateRecords(1245, \"tracks\", \"Addicted to Love\"), tracks should have \"Addicted to Love\" as the last element.');", "updateRecords(2548, \"tracks\", \"\"); assert(!collection[2548].hasOwnProperty(\"tracks\"), 'message: After updateRecords(2548, \"tracks\", \"\"), tracks should not be set');" diff --git a/seed/challenges/01-front-end-development-certification/basic-ziplines.json b/seed/challenges/01-front-end-development-certification/basic-ziplines.json index 6b0a6103877..f80c162894f 100644 --- a/seed/challenges/01-front-end-development-certification/basic-ziplines.json +++ b/seed/challenges/01-front-end-development-certification/basic-ziplines.json @@ -192,7 +192,7 @@ "User Story: I can see thumbnail images of different projects the portfolio creator has built (if you haven't built any websites before, use placeholders.)", "User Story: I navigate to different sections of the webpage by clicking buttons in the navigation.", "Don't worry if you don't have anything to showcase on your portfolio yet - you will build several apps on the next few CodePen challenges, and can come back and update your portfolio later.", - "There are many great portfolio templates out there, but for this challenge, you'll need to build a portfolio page yourself. Using Bootstrap will make this much easier for you.", + "There are many great portfolio templates out there already. However, you should consider building your portfolio page as much as you can from the ground up. Using Bootstrap can help make this process much easier for you.", "Note that CodePen.io overrides the Window.open() function, so if you want to open windows using jQuery, you will need to target invisible anchor elements like this one: <a target='_blank'>.", "Remember to use Read-Search-Ask if you get stuck.", "When you are finished, click the \"I've completed this challenge\" button and include a link to your CodePen. ", diff --git a/seed/challenges/04-video-challenges/jslingo.json b/seed/challenges/04-video-challenges/jslingo.json index b74b2bbb053..89928545ea7 100644 --- a/seed/challenges/04-video-challenges/jslingo.json +++ b/seed/challenges/04-video-challenges/jslingo.json @@ -78,7 +78,7 @@ "title": "JavaScript Lingo: Variables & camelCase", "description": [ "We are going to cover what constitutes a variable, and the reasoning behind camelCase.", - "A variable, also referred to as 'var', is the name or placeholder for a boolean, string, number, or other piece of static information.", + "A variable also referred to as 'var', is the name or placeholder for a boolean, string, number, or another piece of static information.", "You can use Google Dev Tools to inspect the Free Code Camp home page and look for some variables.", "You 'declare' variables the first time with 'var' in front of it, but those can be referenced later in your script.", "camelCase is the way that JavaScript pushes words together and still keeps them legible. The first letter of the first word is lowercase, along with the remainder of the word, but the first letter of every consecutive word is capitalized. There are no spaces. Examples: brianaLovesHerPets, bestFoodIsCheese, and codeIsWorthLearning.", @@ -152,7 +152,7 @@ "id": "56b15f15632298c12f315189", "title": "JavaScript Lingo: Finding and Indexing Data in Arrays", "description": [ - "There are many reasons you might need to access a certain piece of data from within a larger set, and you do that by referencing it's index.", + "There are many reasons you might need to access a certain piece of data from within a larger set, and you do that by referencing its index.", "We won't get into syntax now, but you should know that the first thing in an array is actually index 0.", "This goes for strings and objects, too. All of these indices start at 0, so if you're looking asking the code to find indexArr[2], you're really going to get the third piece of information in that array." ], @@ -185,7 +185,7 @@ "title": "JavaScript Lingo: Manipulating Data", "description": [ "Methods and functions are ways you can manipulate variables or other sets of information.", - "Methods are built in to JavaScript, and you will become more familiar with these as you write functions for the bonfires.", + "Methods are built into JavaScript, and you will become more familiar with these as you write functions for the bonfires.", "I'd suggest looking through the MDN (Mozilla Developer Network that we discussed in the first video) to familiarize yourself with the sheer amount and general potential for the methods that exist. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Methods_Index", "Examples include '.split(' ')', which will turn a string into an array and can do so in many different ways. Methods are powerful - many times when you want your script to do something, there's a method for that.", "Functions, like variables, are something you define and create.", @@ -296,7 +296,7 @@ "description": [ "RegExp is not formatted like anything else in JS, and can have a steep learning curve.", "RegExp can also be an incredibly useful and efficient tool.", - "Using RegExp, you can match, replace, search, and split a string, one of the more difficult types of values to manipulate.", + "Using RegExp, you can match, replace, search, and split a string, one of the most difficult types of values to manipulate.", "Like with all of the other videos, we won't get into the nitty gritty, but I want to show you a few examples of where RegExp is useful.", "If you wanted to create a registration page that verified passwords contained at least a number and a capital letter, you could use RegExp.", "If you wanted to ensure that dates entered in a page were all valid dates in the future, you could use RegExp.", diff --git a/server/boot/a-extendUser.js b/server/boot/a-extendUser.js index 932bd7dcdb5..3e9dbf03057 100644 --- a/server/boot/a-extendUser.js +++ b/server/boot/a-extendUser.js @@ -1,8 +1,10 @@ import { Observable } from 'rx'; import debugFactory from 'debug'; import { isEmail } from 'validator'; +import path from 'path'; const debug = debugFactory('fcc:user:remote'); +const isDev = process.env.NODE_ENV !== 'production'; function destroyAllRelated(id, Model) { return Observable.fromNodeCallback( @@ -15,7 +17,6 @@ module.exports = function(app) { var User = app.models.User; var UserIdentity = app.models.UserIdentity; var UserCredential = app.models.UserCredential; - var Email = app.models.Email; User.observe('before delete', function(ctx, next) { debug('removing user', ctx.where); var id = ctx.where && ctx.where.id ? ctx.where.id : null; @@ -70,21 +71,21 @@ module.exports = function(app) { to: user.email, from: 'Team@freecodecamp.com', subject: 'Welcome to Free Code Camp!', - redirect: '/', - text: [ - 'Greetings from San Francisco!\n\n', - 'Thank you for joining our community.\n', - 'Feel free to email us at this address if you have ', - 'any questions about Free Code Camp.\n', - 'And if you have a moment, check out our blog: ', - 'medium.freecodecamp.com.\n\n', - 'Good luck with the challenges!\n\n', - '- the Free Code Camp Team' - ].join('') + protocol: isDev ? null : 'https', + host: isDev ? 'localhost' : 'freecodecamp.com', + port: isDev ? null : 443, + template: path.join( + __dirname, + '..', + 'views', + 'emails', + 'a-extend-user-welcome.ejs' + ), + redirect: '/' }; debug('sending welcome email'); - return Email.send(mailOptions, function(err) { + return user.verify(mailOptions, function(err) { if (err) { return next(err); } return req.logIn(user, function(err) { if (err) { return next(err); } diff --git a/server/boot/challenge.js b/server/boot/challenge.js index 0325c5248a4..380a6c9b037 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -18,7 +18,8 @@ import { import { observeMethod } from '../utils/rx'; import { - ifNoUserSend + ifNoUserSend, + flashIfNotVerified } from '../utils/middleware'; import getFromDisk$ from '../utils/getFromDisk$'; @@ -419,7 +420,10 @@ module.exports = function(app) { redirectToNextChallenge ); - router.get('/challenges/:challengeName', showChallenge); + router.get('/challenges/:challengeName', + flashIfNotVerified, + showChallenge + ); app.use(router); diff --git a/server/boot/user.js b/server/boot/user.js index 685a2aed5c2..2391f25cd88 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -19,6 +19,8 @@ import { calcLongestStreak } from '../utils/user-stats'; +import { flashIfNotVerified } from '../utils/middleware'; + const debug = debugFactory('fcc:boot:user'); const sendNonUserToMap = ifNoUserRedirectTo('/map'); const certIds = { @@ -183,6 +185,7 @@ module.exports = function(app) { router.get( '/settings', sendNonUserToMap, + flashIfNotVerified, getSettings ); router.get('/vote1', vote1); diff --git a/server/utils/middleware.js b/server/utils/middleware.js index 458168bac3c..7ec17defdfb 100644 --- a/server/utils/middleware.js +++ b/server/utils/middleware.js @@ -28,3 +28,19 @@ export function ifNoUser401(req, res, next) { } return res.status(401).end(); } + +export function flashIfNotVerified(req, res, next) { + const user = req.user; + if (!user) { + return next(); + } + const email = req.user.email; + const emailVerified = req.user.emailVerified; + if (!email || !emailVerified) { + req.flash('info', { + msg: 'Please verify your email address ' + + 'here.' + }); + } + return next(); +} diff --git a/server/views/account/update-email.jade b/server/views/account/update-email.jade index bef48afab99..cb45c9d937b 100644 --- a/server/views/account/update-email.jade +++ b/server/views/account/update-email.jade @@ -15,8 +15,7 @@ block content .form-group input.input-lg.form-control(type='email', name='email', id='email', value=user.email || '', placeholder=user.email || 'Enter your new email', autofocus, required, autocomplete="off") .form-group - button.btn.btn-lg.btn-primary.btn-block(type='submit') - | Update my Email + button.btn.btn-lg.btn-primary.btn-block(type='submit')= !user.email || user.emailVerified ? 'Update my Email' : 'Verify Email' a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='/settings') | Go back to Settings @@ -46,10 +45,10 @@ block content } }) .done(data =>{ - if(data.status && data.status.count){ - $('#flash-content').html("Your email has been updated successfully!"); + if(data && data.message){ + $('#flash-content').html(data.message); $('#flash-board') - .removeClass('alert-danger') + .removeClass('alert-info') .addClass('alert-success') .fadeIn(); } diff --git a/server/views/commit/index.jade b/server/views/commit/index.jade index 722654e64c9..d160d38cdac 100644 --- a/server/views/commit/index.jade +++ b/server/views/commit/index.jade @@ -71,6 +71,7 @@ block content if pledge form.row(name='stop-pledge' action='/commit/stop-commitment' method='post') + input(type='hidden', name='_csrf', value=_csrf) .col-xs-12.col-sm-6.col-sm-offset-3.text-center .button-spacer button.btn.btn-block.btn-lg.btn-default(name='submit' type='submit') Stop my current pledge diff --git a/server/views/emails/a-extend-user-welcome.ejs b/server/views/emails/a-extend-user-welcome.ejs new file mode 100644 index 00000000000..97b0f159cda --- /dev/null +++ b/server/views/emails/a-extend-user-welcome.ejs @@ -0,0 +1,23 @@ +Greetings from San Francisco! +
+
+Thank you for joining our community. +
+
+Please verify your email by following the link below: +
+
+<%= verifyHref %> +
+
+Feel free to email us at this address if you have any questions about Free Code Camp. +
+
+And if you have a moment, check out our blog: https://medium.freecodecamp.com. +
+
+Good luck with the challenges! +
+
+
+- the Free Code Camp Team. diff --git a/server/views/emails/user-email-verify.ejs b/server/views/emails/user-email-verify.ejs new file mode 100644 index 00000000000..6263a89b1f3 --- /dev/null +++ b/server/views/emails/user-email-verify.ejs @@ -0,0 +1,14 @@ +Thank you, for updating you contact details. +
+
+Please verify your email by following the link below: +
+
+<%= verifyHref %> +
+
+Feel free to email us at this address if you have any questions about Free Code Camp. +
+
+
+- the Free Code Camp Team. diff --git a/server/views/partials/footer.jade b/server/views/partials/footer.jade index d529a08b4e3..f67b64325b1 100644 --- a/server/views/partials/footer.jade +++ b/server/views/partials/footer.jade @@ -5,6 +5,8 @@ aside.map-aside.is-collapsed a.map-aside-action-item.map-aside-action-pop-out(href='/map' target='_blank' aria-label='open map in new tab') button.map-aside-action-item.map-aside-action-collapse(aria-label='close map aside') aside.wiki-aside.is-collapsed - .wiki-aside-action-bar + .wiki-aside-action-bar.wiki-fixed-header + .chat-embed-main-title + span Free Code Camp's Wiki a.wiki-aside-action-item.wiki-aside-action-pop-out(href='/wiki' target='_blank' aria-label='open wiki in new tab') button.wiki-aside-action-item.wiki-aside-action-collapse(aria-label='close wiki aside') diff --git a/test/public/js/loop-protect-test.js b/test/public/js/loop-protect-test.js new file mode 100644 index 00000000000..6ec3cbebddc --- /dev/null +++ b/test/public/js/loop-protect-test.js @@ -0,0 +1,15 @@ +'use strict'; + +let loopProtect = require('../../../public/js/lib/loop-protect/loop-protect'); + +let test = require('tape'); + +test('LoopProtect injection', function(t) { + t.plan(1); + + // Label indented 2 spaces - loop indented three spaces + t.true( + loopProtect(' loop1:\n while(true) {\n\n}').indexOf('loop1') > 0, + 'Should keep loop label intact if not lined up with loop.' + ); +}); \ No newline at end of file