diff --git a/01-node-tutorial/.gitignore b/01-node-tutorial/.gitignore index 47f465609f..9f111cc98f 100644 --- a/01-node-tutorial/.gitignore +++ b/01-node-tutorial/.gitignore @@ -1,2 +1,4 @@ /node_modules -/content/big.txt \ No newline at end of file +/content/big.txt +answers/temporary/* +!answers/temporary/.keep \ No newline at end of file diff --git a/01-node-tutorial/answers/.gitignore b/01-node-tutorial/answers/.gitignore new file mode 100644 index 0000000000..90157b1bc5 --- /dev/null +++ b/01-node-tutorial/answers/.gitignore @@ -0,0 +1,2 @@ +/node_modules +.DS_Store \ No newline at end of file diff --git a/01-node-tutorial/answers/01-intro.js b/01-node-tutorial/answers/01-intro.js new file mode 100644 index 0000000000..2dc44d87a3 --- /dev/null +++ b/01-node-tutorial/answers/01-intro.js @@ -0,0 +1 @@ +console.log("Hello from Risqua!") diff --git a/01-node-tutorial/answers/02-globals.js b/01-node-tutorial/answers/02-globals.js new file mode 100644 index 0000000000..84524ac419 --- /dev/null +++ b/01-node-tutorial/answers/02-globals.js @@ -0,0 +1,6 @@ +console.log("Current directory (__dirname):", __dirname); +console.log("MY_VAR environment variable:", process.env.MY_VAR); + +console.log("Current filename (__filename):", __filename); +console.log("Node.js version (process.version):", process.version); +console.log("Platform (process.platform):", process.platform); diff --git a/01-node-tutorial/answers/03-modules.js b/01-node-tutorial/answers/03-modules.js new file mode 100644 index 0000000000..1acf0c7e52 --- /dev/null +++ b/01-node-tutorial/answers/03-modules.js @@ -0,0 +1,12 @@ +const names = require("./04-names.js"); +const sayHi = require("./05-utils.js"); +const altData = require("./06-alternative-flavor.js"); +require("./07-mind-grenade.js"); + +sayHi("Susan"); +sayHi(names.john); +sayHi(names.peter); + +console.log(altData); +console.log("This is the main module running!"); +console.log(altData.item); diff --git a/01-node-tutorial/answers/04-names.js b/01-node-tutorial/answers/04-names.js new file mode 100644 index 0000000000..b7c5309859 --- /dev/null +++ b/01-node-tutorial/answers/04-names.js @@ -0,0 +1,4 @@ +const john = "John"; +const peter = "Peter"; + +module.exports = { john, peter }; diff --git a/01-node-tutorial/answers/05-utils.js b/01-node-tutorial/answers/05-utils.js new file mode 100644 index 0000000000..f0bc6ab8c5 --- /dev/null +++ b/01-node-tutorial/answers/05-utils.js @@ -0,0 +1,5 @@ +const sayHi = (name) => { + console.log(`Hello there, ${name}`); +}; + +module.exports = sayHi; diff --git a/01-node-tutorial/answers/06-alternative-flavor.js b/01-node-tutorial/answers/06-alternative-flavor.js new file mode 100644 index 0000000000..b52fc22eba --- /dev/null +++ b/01-node-tutorial/answers/06-alternative-flavor.js @@ -0,0 +1,4 @@ +module.exports.item = ["item1", "item2"]; +module.exports.person = { + name: "Bob", +}; diff --git a/01-node-tutorial/answers/07-mind-grenade.js b/01-node-tutorial/answers/07-mind-grenade.js new file mode 100644 index 0000000000..0df06c84eb --- /dev/null +++ b/01-node-tutorial/answers/07-mind-grenade.js @@ -0,0 +1,8 @@ +const num1 = 5; +const num2 = 10; + +function addValues() { + console.log(`The sum is: ${num1 + num2}`); +} + +addValues(); diff --git a/01-node-tutorial/answers/16-streams.js b/01-node-tutorial/answers/16-streams.js new file mode 100644 index 0000000000..a65873b22a --- /dev/null +++ b/01-node-tutorial/answers/16-streams.js @@ -0,0 +1,22 @@ +const { createReadStream } = require("fs"); +const path = require("path"); + +const stream = createReadStream(path.join(__dirname, "../content/big.txt"), { + encoding: "utf8", + highWaterMark: 200, +}); + +let chunkCount = 0; + +stream.on("data", (chunk) => { + chunkCount++; + console.log(`Chunk ${chunkCount}:\n`, chunk); +}); + +stream.on("end", () => { + console.log(`Finished. Total chunks: ${chunkCount}`); +}); + +stream.on("error", (err) => { + console.log("Stream error: ", err); +}); diff --git a/01-node-tutorial/answers/17-http-stream.js b/01-node-tutorial/answers/17-http-stream.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/01-node-tutorial/answers/content/first.txt b/01-node-tutorial/answers/content/first.txt new file mode 100644 index 0000000000..3c7d2fbdd6 --- /dev/null +++ b/01-node-tutorial/answers/content/first.txt @@ -0,0 +1 @@ +Hello this is first text file \ No newline at end of file diff --git a/01-node-tutorial/answers/content/second.txt b/01-node-tutorial/answers/content/second.txt new file mode 100644 index 0000000000..f108be0e0e --- /dev/null +++ b/01-node-tutorial/answers/content/second.txt @@ -0,0 +1 @@ +Hello this is second text file \ No newline at end of file diff --git a/01-node-tutorial/answers/content/subfolder/test.txt b/01-node-tutorial/answers/content/subfolder/test.txt new file mode 100644 index 0000000000..a78a2725b8 --- /dev/null +++ b/01-node-tutorial/answers/content/subfolder/test.txt @@ -0,0 +1 @@ +test txt \ No newline at end of file diff --git a/01-node-tutorial/answers/customEmitter.js b/01-node-tutorial/answers/customEmitter.js new file mode 100644 index 0000000000..05e0f1a298 --- /dev/null +++ b/01-node-tutorial/answers/customEmitter.js @@ -0,0 +1,24 @@ +const EventEmitter = require("events"); +const emitter = new EventEmitter(); +setInterval(() => { + emitter.emit("timer", "hi there"); +}, 2000); +emitter.on("timer", (msg) => console.log(msg)); + +/**** + * Or, you could make an async function that waits on an event: + +const EventEmitter = require("events"); +const emitter = new EventEmitter(); +const waitForEvent = () => { + return new Promise((resolve) => { + emitter.on("happens", (msg) => resolve(msg)); + }); +}; +const doWait = async () => { + const msg = await waitForEvent(); + console.log("We got an event! Here it is: ", msg); +}; +doWait(); +emitter.emit("happens", "Hello World!"); + * */ diff --git a/01-node-tutorial/answers/package-lock.json b/01-node-tutorial/answers/package-lock.json new file mode 100644 index 0000000000..08e6bdce86 --- /dev/null +++ b/01-node-tutorial/answers/package-lock.json @@ -0,0 +1,389 @@ +{ + "name": "answers", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "answers", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "nodemon": "^3.1.10" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/01-node-tutorial/answers/package.json b/01-node-tutorial/answers/package.json new file mode 100644 index 0000000000..20f11832de --- /dev/null +++ b/01-node-tutorial/answers/package.json @@ -0,0 +1,17 @@ +{ + "name": "answers", + "version": "1.0.0", + "main": "01-intro.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "nodemon prompter.js", + "start": "node prompter.js" + + }, + "author": "Risqua", + "license": "ISC", + "description": "", + "devDependencies": { + "nodemon": "^3.1.10" + } +} diff --git a/01-node-tutorial/answers/prompter.js b/01-node-tutorial/answers/prompter.js new file mode 100644 index 0000000000..62800fbd3c --- /dev/null +++ b/01-node-tutorial/answers/prompter.js @@ -0,0 +1,67 @@ +const http = require("http"); +var StringDecoder = require("string_decoder").StringDecoder; + +const getBody = (req, callback) => { + const decode = new StringDecoder("utf-8"); + let body = ""; + req.on("data", function (data) { + body += decode.write(data); + }); + req.on("end", function () { + body += decode.end(); + const body1 = decodeURI(body); + const bodyArray = body1.split("&"); + const resultHash = {}; + bodyArray.forEach((part) => { + const partArray = part.split("="); + resultHash[partArray[0]] = partArray[1]; + }); + callback(resultHash); + }); +}; + +// here, you could declare one or more variables to store what comes back from the form. +let item = "Enter something below."; + +// here, you can change the form below to modify the input fields and what is displayed. +// This is just ordinary html with string interpolation. +const form = () => { + return ` + +

${item}

+
+ + +
+ + `; +}; + +const server = http.createServer((req, res) => { + console.log("req.method is ", req.method); + console.log("req.url is ", req.url); + if (req.method === "POST") { + getBody(req, (body) => { + console.log("The body of the post is ", body); + // here, you can add your own logic + if (body["item"]) { + item = body["item"]; + } else { + item = "Nothing was entered."; + } + // Your code changes would end here + res.writeHead(303, { + Location: "/", + }); + res.end(); + }); + } else { + res.end(form()); + } +}); + +server.on("request", (req) => { + console.log("event received: ", req.method, req.url); +}); +server.listen(3000); +console.log("The server is listening on port 3000."); diff --git a/01-node-tutorial/answers/temp.txt b/01-node-tutorial/answers/temp.txt new file mode 100644 index 0000000000..6ad36e52f0 --- /dev/null +++ b/01-node-tutorial/answers/temp.txt @@ -0,0 +1,3 @@ +Line 1 +Line 2 +Line 3 diff --git a/01-node-tutorial/answers/temporary/.keep b/01-node-tutorial/answers/temporary/.keep new file mode 100644 index 0000000000..f59ec20aab --- /dev/null +++ b/01-node-tutorial/answers/temporary/.keep @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/01-node-tutorial/answers/timeStamp.txt b/01-node-tutorial/answers/timeStamp.txt new file mode 100644 index 0000000000..8c1d2e24e2 --- /dev/null +++ b/01-node-tutorial/answers/timeStamp.txt @@ -0,0 +1 @@ +Line 1/nLine 2/nLine 3/n \ No newline at end of file diff --git a/01-node-tutorial/answers/writeWithPromisesAwait.js b/01-node-tutorial/answers/writeWithPromisesAwait.js new file mode 100644 index 0000000000..5f6afdffc8 --- /dev/null +++ b/01-node-tutorial/answers/writeWithPromisesAwait.js @@ -0,0 +1,27 @@ +const { writeFile, readFile } = require("fs").promises; + +const writer = async () => { + try { + await writeFile("temp.txt", "Line 1\n"); + await writeFile("temp.txt", "Line 2\n", { flag: "a" }); + await writeFile("temp.txt", "Line 3\n", { flag: "a" }); + } catch (err) { + console.log("An error occurred during writing: ", err); + } +}; + +const reader = async () => { + try { + const data = await readFile("temp.txt", "utf-8"); + console.log("File contents:\n", data); + } catch (err) { + console.log("An error occurred during reading: ", err); + } +}; + +const readWrite = async () => { + await writer(); + await reader(); +}; + +readWrite(); diff --git a/01-node-tutorial/answers/writeWithPromisesThen.js b/01-node-tutorial/answers/writeWithPromisesThen.js new file mode 100644 index 0000000000..7337f2ed11 --- /dev/null +++ b/01-node-tutorial/answers/writeWithPromisesThen.js @@ -0,0 +1,10 @@ +const { writeFile, readFile } = require("fs").promises; + +writeFile("temp.txt", "Line 1\n") + .then(() => writeFile("temp.txt", "Line 2\n", { flag: "a" })) + .then(() => writeFile("temp.txt", "Line 3\n", { flag: "a" })) + .then(() => readFile("temp.txt", "utf-8")) + .then((data) => console.log("File contents:\n", data)) + .catch((error) => { + console.log("An error occurred: ", error); + }); diff --git a/02-express-tutorial/.gitignore b/02-express-tutorial/.gitignore index 30bc162798..07e6e472cc 100644 --- a/02-express-tutorial/.gitignore +++ b/02-express-tutorial/.gitignore @@ -1 +1 @@ -/node_modules \ No newline at end of file +/node_modules diff --git a/02-express-tutorial/app.js b/02-express-tutorial/app.js index ce296a6ee3..98bfd1c341 100644 --- a/02-express-tutorial/app.js +++ b/02-express-tutorial/app.js @@ -1 +1,59 @@ -console.log('Express Tutorial') +//console.log('Express Tutorial') +const express = require("express"); // Import express +const app = express(); // Create app instance +const { products } = require("./data"); + +app.use(express.static("./public")); //to serve static files like index.html + +//to test the route +app.get("/api/v1/test", (req, res) => { + res.json({ message: "It worked!" }); +}); + +//Route to return all products +app.get("/api/v1/products", (req, res) => { + res.json(products); +}); + +//Route to return a single product by ID +app.get("/api/v1/products/:productID", (req, res) => { + const idToFind = parseInt(req.params.productID); // Convert ID from string to number + const product = products.find((p) => p.id === idToFind); + + if (!product) { + return res.status(404).json({ message: "That product was not found." }); + } + + res.json(product); +}); + +// Route to handle query string filtering +app.get("/api/v1/query", (req, res) => { + const { search, limit, price } = req.query; + let result = [...products]; // Copy original array + + if (search) { + result = result.filter((product) => + product.name.toLowerCase().startsWith(search.toLowerCase()) + ); + } + + if (price) { + result = result.filter((product) => product.price < parseFloat(price)); + } + + if (limit) { + result = result.slice(0, parseInt(limit)); + } + + res.json(result); +}); + +//catch all error +app.all("*", (req, res) => { + res.status(404).send("Page not found"); +}); + +app.listen(3000, () => { + console.log("Server is listening on port 3000..."); +}); diff --git a/02-express-tutorial/package-lock.json b/02-express-tutorial/package-lock.json index 417394ff8b..8bb5c6b82e 100644 --- a/02-express-tutorial/package-lock.json +++ b/02-express-tutorial/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "2-express-tutorial", "version": "1.0.0", "license": "ISC", "dependencies": { diff --git a/02-express-tutorial/public/fetch-products.js b/02-express-tutorial/public/fetch-products.js new file mode 100644 index 0000000000..6450c7960a --- /dev/null +++ b/02-express-tutorial/public/fetch-products.js @@ -0,0 +1,16 @@ +document.getElementById("loadProducts").addEventListener("click", async () => { + const res = await fetch("/api/v1/products"); + const data = await res.json(); + + const output = document.getElementById("output"); + output.innerHTML = data + .map( + (p) => ` +
+ ${p.name} +

${p.name} - $${p.price}

+
+ ` + ) + .join(""); +}); diff --git a/02-express-tutorial/public/index.html b/02-express-tutorial/public/index.html new file mode 100644 index 0000000000..5f680a8784 --- /dev/null +++ b/02-express-tutorial/public/index.html @@ -0,0 +1,12 @@ + + + + My Express App + + +

Welcome to My Express App

+ +
+ + + diff --git a/03-task-manager/optionalArrayMethodsReviewExtraAssignment.js b/03-task-manager/optionalArrayMethodsReviewExtraAssignment.js new file mode 100644 index 0000000000..9b269e25a9 --- /dev/null +++ b/03-task-manager/optionalArrayMethodsReviewExtraAssignment.js @@ -0,0 +1,328 @@ +// Review of JavaScript iterative Array methods (`.map`, `.filter` and `.forEach`) + +// This is an optional extra assignment + +///////////////////////////// Questions /////////////////////////////////////// + +// First, some basic background knowledge questions that we discussed during +// the mentor session + +//////////////////// +// What is an array? +// +// An array is a data structure which contains a sequence of potentially mixed +// data types. Structures with multiple elements are sometimes called +// "collections" + +const stuff = [1, 2, 'fish', { id: 3 }]; + + +////////////////////////////// +///// What is a method? ///// +// +// It is a function that operates on a SPECIFIC data structure. This is a +// word from Object Oriented Programming (sometimes called OOP). +// +// In OOP, we consider functions and data as a hybrid "thing." We call those +// "things" objects. In JavaScript, EVERYTHING is an object, including +// functions! But that's kinds of besides the point. The important thing is +// the conceptual idea: data + behavior is thought of as "one thing." + +const object = { + // in OO, we group data and behavior + data: 1, + behavior: function() { + // "this" is a reference to the object we are inside of right now + this.data++; + console.log('OOP demo:', this.data); + } +} + +// Every time we call `.behavior()`, the data (number) inside `object` is +// incremented by 1, so we print "OOP demo 1", "OOP demo 2", etc. + +object.behavior() +object.behavior() +object.behavior() +object.behavior() +object.behavior() + +// That's why we call them "array methods" they are methods that exist on the +// "Array" object. + +///////////////////////////////////////////////// +///// What are the common JS array methods? ///// +// +// - Array.prototype.push (does not take a callback) +// +// These guys all take a callback as input, and then call the callabck for +// each item in the array +// +// - Array.prototype.filter +// - The callback should return a boolean. If the return value is true, the +// element becomes a member of the new array. If the return value is false, +// the element is filtered (removed). + + const integers = [1, 2, 3, 4, 5]; +// evenNumbers will be interger % 2 for each integer +// '%' is the "modulo" operator. Here we are checking if `integer` divided by 2, leaves a remainder of 0, which is true for even numbers and false for odd numbers. + const evenNumbers = integers.filter((integer) => { + return integer % 2 === 0 + }) + +// - Array.prototype.map +// - The callback recieves each item of the array. The return value is pushed +// into a new array + const numbers = [1, 2, 3]; + const doubles = numbers.map((i) => i * 2); + +// - Array.prototype.forEach +// - `forEach` is like a "for" loop. It calls the callback for every item in +// the array + evenNumbers.forEach((thingy) => console.log('even', thingy)); + doubles.forEach((d) => console.log('doubled!', d)); + +// - Array.prototype.reduce +// - A bit tricky +// - Can transform an array into an atribrary result + const lastNames = ['Smith', 'Toure', 'Hernandez'] + const initialValue = 0; + const totalLettersInNames = lastNames.reduce((runningTotal, currentName) => { + return runningTotal + currentName.length; + }, initialValue) + console.log({totalLettersInNames}); + + // The first argument is always the return value that we're building up. + // I called it, "runningTotal" before. The default name is "accumulator." + // Often, Array.prototype.reduce is used to build a mapping from an array, + // like this: + const people = [{id: 1, name: 'tim'}, {id: 2, name: 'jane'}]; + const peopleIdMap = people.reduce((map, person) => { + map[person.id] = person; + return map; + }, {} /* second arg is always the initial value! Here, it's an empty object */); + + // Now we can lookup people by id! + console.log({lookedUpPerson1: peopleIdMap[1]}) + console.log({lookedUpPerson2: peopleIdMap[2]}); + + // Sometimes, you'll see this fancy syntax used with reduce, especially when + // building mappings. Beware, though, there's a lot of unnecessary runtime + // overhead here, because we create a new object here every time instead of + // re-using the old one!! And it's only a few characters shorter than the + // more performant solution above. + const peopleNameMap = people.reduce((map, person) => ({ + ...map, + [person.name]: person + }), {}); + + // Now we can lookup people by name! + console.log({lookupTim: peopleNameMap['tim']}) + console.log({lookupJane: peopleNameMap['jane']}); + +/////////////////////////// CHALLENGES //////////////////////////////////////// + +// Each challenge will be related to this array of names. It will pose a +// problem related to these names, and then implement the solution. The +// challenges are: +// +// - Create a new array with only each person's last name +// - Filter names that don't match the format " " +// - Should remove Tam because she has a double-space +// - Should remove Carlow because he has a middle-name +// - Should also remove names like: +// - "Timothy Cook" +// - "Nick_Masters" +// - "Timmy-Turner" +// - "Billy\nBob" +// - etc. +// - Create a new array where everyone's name is converted to "Title Case" +// - The first character of each word should be uppercase +// - All other characters in the word should be lowercase +// - expected output is ['Dimitry Santiago', 'Carlos D. Perez', 'Tam Person', ...] +// - Last Challenge: +// Remove names with the wrong format +// AND change it to "Title Case" +// AND remove people whose last name ends with z +// AND write a message asking them to sign up +// +// For an extra assignment, you may implement these yourself! Include your +// changes to this file with your MR for week 3. + +const names = [ + 'Dimitry SantiAgo', + 'Carlos d. Perez', + 'tam person', + 'Mariana Gomez', + 'Amy You' +]; + +/////////////////////////////////////////////////////////////////////////////// +//// put your answers above if you wish to do the challenges on your own ////// +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//////////////////// //////////////////// +//////////////////// ! ! //////////////////// +//////////////////// ! Read no further if you wish to ! //////////////////// +//////////////////// ! do the extra assignment ! //////////////////// +//////////////////// ! ! //////////////////// +//////////////////// //////////////////// +/////////////////////////////////////////////////////////////////////////////// +/////// and maybe also delete or comment-out the remainder of this file! ////// +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// + +//////// CHALLENGE: Get everyone's last name +const everyonesLastName = names.map((name) => { + // `.map` can transform each element 1:1 + const eachWordSeparated = name.split(" ") + // how to get the last index from JS array + const lastName = eachWordSeparated.pop(); + return lastName; +}); +console.log('everyone last name', everyonesLastName); + +//////// CHALLENGE: Filter to the people who followed the right +// "right format" is " " with a single space! +const rightFormat = /^\w+ \w+$/; +const matchesTeachersPedanticFormattingRule = names.filter((name) => { + return name.match(rightFormat); +}); +console.log('good students', matchesTeachersPedanticFormattingRule) +// (joke :) + + +//////// CHALLENGE: Change everyone's name to "Title Case" +// (Each Word Uppercase) + +// Time complexity is O(n^3): AKA very slow!! This is not an ideal solution in +// terms of performance, but it does a great job of stretching our understanding +// of Array.map + +// The next section will breakdown this example in much greater detail! + +const titledNames = names.map((name) => { + // `.map` can transform each element 1:1 + const eachWordSeparated = name.split(" ") + + const titledName = eachWordSeparated.map((inputWord) => { + const inputLetters = inputWord.split(""); + const wordWithFirstLetterUppercase = inputLetters + .map((letter, idx) => ( + idx === 0 + ? letter.toUpperCase() + : letter.toLowerCase() + )) + .join("") + return wordWithFirstLetterUppercase + }); + return titledName.join(" ") +}); +console.log('titledNames', titledNames); + +// Same example as above (change every name to title case), but I'll break it +// up into smaller pieces to make it more readable. Each callback function +// which was "inlined" before are now defined as separate functions, given a +// name, and documented + +/** + * This is the callback for the innermost map. Map functions always take two + * parameters: the array element, and the index of the array element. + * + * In this case, we give these 2 pieces meaningful names: characterInWord, + * and indexOfCharacter. Remember POSITIONAL arguments (like `(item, index)`) + * are identified by POSITION. As long as a POSITIONAL argument is in the + * correct POSITION you can give it any name. The best practice is to use + * the most descriptive and clear names you can, which we've done here. + */ +const transformWordIntoTitle = (characterInWord, indexOfCharacter) => { + // We only want to change the FIRST letter of the word to uppercase + if (indexOfCharacter === 0) { + return characterInWord.toUpperCase(); + } // we have returned!! The rest of the code will ONLY run for characters + // after the first one + + // We could skip `.toLowerCase`, but if a letter in the middle of the word + // is uPpErcAse, it'll look nicer if we transform it into lowercase + return characterInWord.toLowerCase(); +} + +/** + * This is the callback used when we are mapping over an array of "words," like: + * + * ``` + * ["Carlos", "d.", "Perez"] + * ``` + * + * This function receives a string (just ONE of those words, like "d."). + * + * It will split the word up into an array of letters, use the map function + * from before to transform that array into title-case, then join that + * transformed array back into a string, and return the result. + * + * This is the most wasteful & superfluous step. You probably notice we could + * just do this instead: + * + * ``` + * firstLetter = wordInString[0]; + * otherLetters = wordInString.splice(1); + * return `${firstLetter.toLowerCase()}${otherLetters.toLowerCase()}` + * ``` + * + * Indeed, this would be much faster since we avoid an inner loop, but our goal + * is to learn, not to go fast! + */ +const transformStringIntoTitledWords = (wordInString) => { + const letters = wordInString.split(''); + const titleCaseLetters = letters.map(transformWordIntoTitle); + return titleCaseLetters.join(''); +} + +/** + * Finally, the highest level: this callback operates on every string in our + * main name array for this example. It breaks the name up into an array of + * words first: + * + * ``` + * "carlos cantiago" + * -> ["carlos", "cantiago"] + * -> ["Carlos", "Santiago"] + * -> "Carlos Santiago" + * ``` + */ +const transformNameIntoTitleCase = (name) => { + // We'll use a regex to split the string. ' +' means "one or more spaces." + // This is good because it'll work for our name "tam person" where there is + // a double-space + const nameWords = name.split(/ +/); + const titleCaseWords = nameWords.map(transformStringIntoTitledWords) + return titleCaseWords.join(' '); +} + +console.log( + 'titledNames verbose', + names.map(transformNameIntoTitleCase) +) + + +//////// CHALLENGE: Remove names with the wrong format +// AND change it to "Title Case" +// AND remove people whose last name ends with z +// AND write a message asking them to sign up +const result = names + // remove bad format + .filter((name) => name.match(rightFormat)) + // change to title case + .map(transformNameIntoTitleCase) + // remove names that end in "z" + .filter((name) => { + const lastLetter = name.slice(-1); + return lastLetter.toLowerCase() !== 'z' + }) + // transform into a sign-up message + .map((name) => ` + Hey there ${name}! + Want to buy my thing? + `); + +console.log('result', result); diff --git a/lessons/ctd-node-assignment-1.md b/lessons/ctd-node-assignment-1.md new file mode 100644 index 0000000000..f75b13af08 --- /dev/null +++ b/lessons/ctd-node-assignment-1.md @@ -0,0 +1,101 @@ +You should already have done the steps described in the **[Getting Started page](./getting-started-with-node-development.md)**. That page describes how to get git, the VSCode Editor, Node, and Postman all installed. All of those should be installed before you start this lesson. + +The next step is to create a “fork” of your starter repository for this lesson, which is found **[here](https://github.com/Code-the-Dream-School/node-express-course)**. The fork button is on the upper right of that page. Once the fork is complete, you must `git clone` your fork to get the repository files onto your computer. + +Careful! + +Please **don’t clone the original Code-the-Dream repository**, +as if you do that, you will not be able to push your work to Github. + +![Fork repo screenshot](./images/fork-button.png) + +![Git clone command screenshot](./images/git-clone.png) + +You will do all of your work inside the directory created by the `git clone` command. By default, this directory will be called “node-express-course”. Change directories so that you are inside that directory. Then create the **branch** for this week, using the command `git checkout -b week1`. + +Now change the directory to the one that says `01-node-tutorial/answers`. You’ll do all of this week’s work inside this directory. + +Create the following programs for this lesson, all within the “answers” directory. By the way, there are examples of each of the programs you need to create in the 01-node-tutorial directory (in case you get stuck) but try to do your own work. If you need to review a section of the video for any of these exercises, view the video within Youtube, but not in full screen mode. The panel on the right will show you the chapter of the video so that you know what you should review. + +Your homework should include the following programs: + +1. `01-intro.js`: This program should use the `console.log` function to write something to the screen. While you are in the “answers” directory, run the command, `node 01-intro.js`, to verify that the program runs. You can also put additional JavaScript logic in your program. +2. `02-globals.js`: This program should use the `console.log` function to write some globals to the screen. Set an environment variable with the following command in your command line terminal: `export MY_VAR="Hi there!"` The program should then use `console.log` to print out the values of `__dirname` (a Node global variable) and `process.env.MY_VAR` (`process` is also a global, and contains the environment variables you set in your terminal.) You could print out other globals as well ([Node documentation](https://nodejs.org/api/globals.html#global-objects) on available globals). For each of these programs, you invoke them with `node` to make sure they work. +3. For the next part, you will write multiple programs. `04-names.js`, `05-utils.js`, `06-alternative-flavor.js`, and `07-mind-grenade.js` are modules that you load, using require statements, from the `03-modules.js` file, which is the main program. Remember that you must give the path name in your require statement, for example: + +```javascript +const names = require("./04-names.js"); +``` + +(3a). `04-names.js` should export multiple values in an object that you will require in `03-module.js`. + +(3b). `05-utils.js` should export a single value, which is a function you will call in `03-modules.js`. + +(3c). `06-alternative-flavor.js` should export multiple values in the module.exports object, but it should use the alternative approach, adding each value one at a time. The exported values from each should be used in `03-modules.js`, logging results to the console so that you know it is working. + +(3d). `07-mind-grenade.js` may not export anything, but it should contain a function that logs something to the console. You should then call that function within the code of `07-mind-grenade.js`. This is to demonstrate that when a module is loaded with a require statement, anything in the mainline code of the loaded module runs. +**NOTE**: The only program you should need to actually invoke to test that everything is working is `03-modules.js`, because it loads all the others (files 4-7). + +1. `08-os-module.js`: This should load the built-in `os` Node module and display some interesting information from the resulting object. As for all modules, you load a reference to it with a require statement, in this case + +```javascript +const os = require("os"); +``` + +You can look **[here](https://nodejs.org/api/os.html)** for documentation on the stuff in the built-in os module. + +1. `09-path-module.js`: This should load the `path` Node module, which is another built-in module. It should then call the `path.join` function to join up a sequence of alphanumeric strings, and it should print out the result. The result will work one way on Windows, where the directory separator is a backslash, and a different way on other platforms, where the directory separator is a slash. + +``` +# Example of a Windows path: +C:\Users\JohnSmith\node-express-course\01-node-tutorial\answers + +# Exmaple of a Mac or Linux path: +/Users/JohnSmith/node-express-course/01-node-tutorial/answers +``` + +1. `10-fs-sync.js`: This should load `writeFileSync` and `readFileSync` functions from the `fs` module. Then you will use `writeFileSync` to write 3 lines to a file, `./temporary/fileA.txt`, using the `"append"` flag for each line after the first one. Then use `readFileSync` to read the file, and log the contents to the console. Be sure you create the file in the `temporary` directory. That will ensure that it isn’t pushed to Github when you submit your answers (because that file has been added to the `.gitignore` file for you already which tells git not to look at those files). +2. `11-fs-async.js`: This should load the `fs` module, and use the asynchronous function `writeFile` to write 3 lines to a file, `./temporary/fileB.txt`. Now, be careful here! This is our first use of **asynchronous functions** in this class, but we are going to use them a lot! First, you need to use the `"append"` flag for all but the first line. Second, each time you write a line to the file, you need to have a callback, because the `writeFile` operation is asynchronous. Third, for each line you write, you need to do the write for the line that follows it within the callback – otherwise the operations won’t happen in order. Put `console.log` statements at various points in your code to tell you when each step completes. Then run the code. Do the console log statements appear in the order you expect? Run the program several times and verify that the file is created correctly. Here is how you might start: + +```javascript +const { writeFile } = require("fs"); +console.log("at start"); +writeFile("./temporary/output.txt", "This is line 1\n", (err, result) => { + console.log("at point 1"); + if (err) { + console.log("This error happened: ", err); + } else { + // here you write your next line + } +}); +console.log("at end"); +``` + +To get the lines to be written in order, you end up with a long chain of callbacks, which is called “callback hell”. We’ll learn a better way to do this soon. + +1. `12-http.js`. This program should use the built-in `http` module to create a simple web server that listens on port 3000\. This is done with the `createServer` function. You pass it a callback function that checks the request variable (`req`) for the current `url` property, and depending on what the URL is, sends back a message to the browser screen. Then have your code listen on port 3000, run this file with the `node` command, and test it from your browser, by navigating to `http://localhost:3000`. You can look at `12-http.js` for the instructor’s answer (except that program listens on 5000). You will need to type in Ctrl+c (the Ctrl key plus the letter “C” at the same time; or for Mac the Cmd key plus the letter “C” at the same time) to exit your program. +2. Within your “answers” directory is a program called `prompter.js`. This is a program for a simple server. Try it out! It will display a form in the browser when you run the file and navigate to `http://localhost:3000`. Then, when the user submits the form, it echoes back what was submitted, and displays the form again. You don’t have to worry about how it works. There is a simple body parser to read any values submitted with the form, and that parser returns a hash with the name and value of each. Because the parser is asynchronous, you get back the hash in a callback. +Now, your task is to change this program so that it does something interesting! First, you can change the variables that you want to store when you get the form back. Then, you can change the form itself to return the values you want from the user, which you store in those variables. Then, you can use string interpolation to insert the values of your variables into the HTML. Finally, you change the logic that handles the hash of values you get when the user submits the form, so that you save the values the user submits. The places you would change are marked in the code. +For example, you could change the input field to be a dropdown with various colors, and you could set the background color of the body to be what the user chooses. Or, you could make a number guessing game: Start with a random number from 1 to 100, let the user guess, and tell the user if their guess is low or high. In this case, you’d change the input field so that it accepts only numeric input (but when it is returned in the hash, it will be a string, so you’d have to convert it.) + +When you are done, change directories to the `01-node-tutorial` folder and then do the following to submit your work: + +``` +git commit add ./answers +git commit -m "answers for lesson 1" +git push -u origin week1 +``` + +Then go to your GitHub repository – the one you created with a fork (the URL should have your Github username in it). Create a pull request. You may see a yellow banner on your repository if you recently pushed your change, where you can click the “Compare & pull request” to create a PR. Otherwise, you can switch to your branch in the dropdown on the repo page, and then click the pull request icon to create a PR that way. + +Careful! + +The target of the pull request should be the main branch of the repository +you created when you forked — +**not the Code-the-Dream-School main branch!** + +Once you have created the pull request, you can copy the URL to link the pull request in your homework submission form. The homework submission form is on the main page for this lesson. This will be the general procedure for submitting homework for this course. + + + +When you submit your homework, you also submit your answers to the mindset assignment, if there is one for that lesson. diff --git a/lessons/ctd-node-assignment-10.md b/lessons/ctd-node-assignment-10.md new file mode 100644 index 0000000000..2134518689 --- /dev/null +++ b/lessons/ctd-node-assignment-10.md @@ -0,0 +1,24 @@ +Complete and test all CRUD operations for your model (or the `Jobs` model). Be sure that each operation uses the ID of the user, so that you have good access control. + +**Test each step with Postman, creating a Postman collection of tests just like the instructor is doing.** + +### Deploying to Render.com + +You deploy your application once you have your assignment completed and working, and once you have pushed your `week10` branch to Github. + +To deploy to Render.com, follow these steps: + +1. Specify the version of Node that Render is to use. One way is to create a `.node-version` file in the root of your project repository, specifying the same version of node as you are running on your machine. On my machine, when I type `node -v`, I get back v16.19.0 . So I would create a `.node-version` file with the single line: `16.19.0` +2. Create a [Render.com](https://render.com/) account. You do not need to install anything on your workstation for Render deployment. You do not need to enter any credit card information. +3. From your Render.com dashboard, click on the New button in the upper right. Select Web Service. You will then be prompted to connect a repository. Scroll down to the entry field that says “public git repository”, and enter the URL of your 06-jobs-api repository. Then press continue. +4. The next page prompts you for the name of the service. This becomes the first part of the URL for your application. You need to give it a unique name that no one else is using, maybe something like `jobs-api-`. +5. Scroll down to the entry field for branch. Put in `week10`. +6. Scroll down until you see the “advanced” button. Click on that. You then click on Add Environment Variable. You need to add an environment variable for each of the values in your .env file: the Mongo URI, the JWT key, and so on. +7. Scroll to the bottom and click on Create Web Service. Render.com then builds the application for deployment. The process takes a while. Once the process completes, you see a link in the upper right for your new web service. The URL will be something like `https://jobs-api-.onrender.com`. +8. Copy that URL and put it into your Postman tests for the assignment. Then test with Postman. Your application is live! + + + +### Swagger / OpenAPI documentation + +The section of the video from 9:34:30 to the end discusses setting up a Swagger configuration. When you have an API, you need to document it so that implementers of applications that call the API (like the frontend) know what the available endpoints and operations are. Swagger is the best way to do that. It also creates a graphical user interface so that one can call the APIs directly from the UI. You should watch this section so that you understand how a Swagger configuration may be created and what functions it provides. However, this section of the video gets a bit complicated, so you are not required to implement Swagger for your application, but it’s a great idea to watch this section and familiarize yourself with the concept of Swagger. If possible, try to implement it as a bonus task this week. \ No newline at end of file diff --git a/lessons/ctd-node-assignment-11.md b/lessons/ctd-node-assignment-11.md new file mode 100644 index 0000000000..cb16052697 --- /dev/null +++ b/lessons/ctd-node-assignment-11.md @@ -0,0 +1,861 @@ +Continue working in the `06-jobs-api` repository that you used for lessons 9 and 10\. Before you start, create a new branch as usual, with a branch name of `week12`. (Week 11 was a catch-up week, so you create the `week12` branch when the `week10` branch is active.) + +Create a directory called `public`. This is for the HTML and JavaScript files for the front end. The HTML code you will use is below. Put that in the `public` directory with a file name of `index.html`. (**Reminder:** You will have to change the form, which below is for a job, to match your data model). + +This lesson does not involve the creation of any new Node or Express functions. It is all client-side HTML and JavaScript. + +``` + + + + + + + Jobs List + + + +

Jobs List

+
+

+
+ + + + + + + +``` + +This front end uses a single-page style. There are multiple views in the page, in different DIV elements, but which of these is visible is controlled by the JavaScript you write. + +Edit `app.js`. Comment out the following lines: + +```javascript +app.get("/", (req, res) => { + res.send('

Jobs API

Documentation'); +}); +``` + +Add the following line below these commented out lines: + +```javascript +app.use(express.static("public")); +``` + +Start the server and go to `localhost:3000` in your browser. You see the page, but it does not do anything. This is because there is no JavaScript to go with it. You create that now. This is the JavaScript for the front end. **Front end JavaScript does not run in Node. Instead, it is delivered to the browser and runs in the browser context, with full access to the `document`, `window`, and `DOM` variables.** + +There are various divs to be manipulated by this JavaScript. (Again, you have to change this code to match your data model.) The keep the code organized, we will separate them into various modules (files). There is one module, index.js, to initialize window handling. All the other modules can be imported from there. There are five divs, only one of which will show at a time: + +* one to select logon or register, +* one to do logon, +* one to do register, +* one to display the jobs, +* and one to add or edit a job. + +For each of these divs, there is a separate module controlling its operation. + +To begin, add the following line to index.html, right above the close of the body tag: + +``` + +``` + +These modules call one another using the exports that each provides. For this to work, you must declare it as type `module`. Create `index.js` in the public directory. The `index.js` module should read as follows: + +```javascript +let activeDiv = null; +export const setDiv = (newDiv) => { + if (newDiv != activeDiv) { + if (activeDiv) { + activeDiv.style.display = "none"; + } + newDiv.style.display = "block"; + activeDiv = newDiv; + } +}; + +export let inputEnabled = true; +export const enableInput = (state) => { + inputEnabled = state; +}; + +export let token = null; +export const setToken = (value) => { + token = value; + if (value) { + localStorage.setItem("token", value); + } else { + localStorage.removeItem("token"); + } +}; + +export let message = null; + +import { showJobs, handleJobs } from "./jobs.js"; +import { showLoginRegister, handleLoginRegister } from "./loginRegister.js"; +import { handleLogin } from "./login.js"; +import { handleAddEdit } from "./addEdit.js"; +import { handleRegister } from "./register.js"; + +document.addEventListener("DOMContentLoaded", () => { + token = localStorage.getItem("token"); + message = document.getElementById("message"); + handleLoginRegister(); + handleLogin(); + handleJobs(); + handleRegister(); + handleAddEdit(); + if (token) { + showJobs(); + } else { + showLoginRegister(); + } +}); +``` + +Remember that this code is running in the browser, and not in Node. That means you must use `import` and not `require`. + +We need to keep track of the active div so that we know which one to disable when switching between them, and that is stored in the variable `activeDiv`. We don’t need to export that variable since it’s only used here in the index script by the `setDiv` function. We export a function that sets the active div, making it visible and hiding the previous active div. + +We also have to have a means of enabling or disabling input. This is because we will use asynchronous functions, and the application can get confused if more input comes in while the previous requests are in progress. + +We also have to keep track of whether the user is logged in. We do that in a `token` variable that we store in the browser’s local storage (although this creates security risks as previously described.) When local storage is used, the user remains logged in even if the page is refreshed. If the function is called with a `null` token, then we _remove_ the token from local storage instead. + +When the user takes actions we may want to display a message on the page. We store the value of that message here in the index script in the `message` variable, so that it can easily be updated by any of the other modules. + +Once the DOM is loaded, we load the token (if it exists already) from the browser’s local storage and initialize the handlers for each of the divs. + +Then, if the user is logged in, we display the list of jobs. If the user is not logged in, we display the initial panel with a button for logon and a button for register. Note that we need to provide and to export functions to set the enabled flag and the token. This is because one can’t write directly to variables from other modules. Once a variable is `import`ed in a module, it is treated as a `const` variable in that module, so you cannot reassign values to that variable directly. + +You will need to create `loginRegister.js`, `register.js`, `login.js`, `jobs.js`, and `addEdit.js` files, all in the public directory. + +The `loginRegister.js` module is as follows: + +```javascript +import { inputEnabled, setDiv } from "./index.js"; +import { showLogin } from "./login.js"; +import { showRegister } from "./register.js"; + +let loginRegisterDiv = null; + +export const handleLoginRegister = () => { + loginRegisterDiv = document.getElementById("logon-register"); + const login = document.getElementById("logon"); + const register = document.getElementById("register"); + + loginRegisterDiv.addEventListener("click", (e) => { + if (inputEnabled && e.target.nodeName === "BUTTON") { + if (e.target === login) { + showLogin(); + } else if (e.target === register) { + showRegister(); + } + } + }); +}; + +export const showLoginRegister = () => { + setDiv(loginRegisterDiv); +}; +``` + +Each of the div handling modules follow this pattern. Required imports (used when one div handler calls another) are resolved up front. Then, within the handler function, the div and its controls are defined. Also, within the handler function, an event handler is declared to handle mouse clicks within the div. + +A separate function handles display of the div. (React works in similar fashion, if you know that framework — but this lesson does not use React.) + +The `register.js` module is as follows: + +```javascript +import { + inputEnabled, + setDiv, + message, + token, + enableInput, + setToken, +} from "./index.js"; +import { showLoginRegister } from "./loginRegister.js"; +import { showJobs } from "./jobs.js"; + +let registerDiv = null; +let name = null; +let email1 = null; +let password1 = null; +let password2 = null; + +export const handleRegister = () => { + registerDiv = document.getElementById("register-div"); + name = document.getElementById("name"); + email1 = document.getElementById("email1"); + password1 = document.getElementById("password1"); + password2 = document.getElementById("password2"); + const registerButton = document.getElementById("register-button"); + const registerCancel = document.getElementById("register-cancel"); + + registerDiv.addEventListener("click", (e) => { + if (inputEnabled && e.target.nodeName === "BUTTON") { + if (e.target === registerButton) { + showJobs(); + } else if (e.target === registerCancel) { + showLoginRegister(); + } + } + }); +}; + +export const showRegister = () => { + email1.value = null; + password1.value = null; + password2.value = null; + setDiv(registerDiv); +}; +``` + +The `login.js` module is as follows: + +```javascript +import { + inputEnabled, + setDiv, + token, + message, + enableInput, + setToken, +} from "./index.js"; +import { showLoginRegister } from "./loginRegister.js"; +import { showJobs } from "./jobs.js"; + +let loginDiv = null; +let email = null; +let password = null; + +export const handleLogin = () => { + loginDiv = document.getElementById("logon-div"); + email = document.getElementById("email"); + password = document.getElementById("password"); + const logonButton = document.getElementById("logon-button"); + const logonCancel = document.getElementById("logon-cancel"); + + loginDiv.addEventListener("click", (e) => { + if (inputEnabled && e.target.nodeName === "BUTTON") { + if (e.target === logonButton) { + showJobs(); + } else if (e.target === logonCancel) { + showLoginRegister(); + } + } + }); +}; + +export const showLogin = () => { + email.value = null; + password.value = null; + setDiv(loginDiv); +}; +``` + +The `jobs.js` module is as follows: + +```javascript +import { + inputEnabled, + setDiv, + message, + setToken, + token, + enableInput, +} from "./index.js"; +import { showLoginRegister } from "./loginRegister.js"; +import { showAddEdit } from "./addEdit.js"; + +let jobsDiv = null; +let jobsTable = null; +let jobsTableHeader = null; + +export const handleJobs = () => { + jobsDiv = document.getElementById("jobs"); + const logoff = document.getElementById("logoff"); + const addJob = document.getElementById("add-job"); + jobsTable = document.getElementById("jobs-table"); + jobsTableHeader = document.getElementById("jobs-table-header"); + + jobsDiv.addEventListener("click", (e) => { + if (inputEnabled && e.target.nodeName === "BUTTON") { + if (e.target === addJob) { + showAddEdit(null); + } else if (e.target === logoff) { + showLoginRegister(); + } + } + }); +}; + +export const showJobs = async () => { + setDiv(jobsDiv); +}; +``` + +The `addEdit.js` module is as follows: + +```javascript +import { enableInput, inputEnabled, message, setDiv, token } from "./index.js"; +import { showJobs } from "./jobs.js"; + +let addEditDiv = null; +let company = null; +let position = null; +let status = null; +let addingJob = null; + +export const handleAddEdit = () => { + addEditDiv = document.getElementById("edit-job"); + company = document.getElementById("company"); + position = document.getElementById("position"); + status = document.getElementById("status"); + addingJob = document.getElementById("adding-job"); + const editCancel = document.getElementById("edit-cancel"); + + addEditDiv.addEventListener("click", (e) => { + if (inputEnabled && e.target.nodeName === "BUTTON") { + if (e.target === addingJob) { + showJobs(); + } else if (e.target === editCancel) { + showJobs(); + } + } + }); +}; + +export const showAddEdit = (job) => { + message.textContent = ""; + setDiv(addEditDiv); +}; +``` + +Create all these files and then try the application out. You will find that the application now allows for user interaction, and you can navigate between the active divs. + +However, the application still does not do much, because, of course, there is no code there to communicate with the back end. This is to be added using the `fetch()` function. Fetch makes REST calls, and it returns results asynchronously. In the code that follows, we use the `async/await` pattern, so of course whenever that is used, the surrounding function must be declared as an `async` function. + +Also, we need to disable input during the period in which the `async` operation is in progress. We do this by setting the `inputEnabled` flag using the `enableInput` function that is exported from `index.js`. Our click handlers ignore clicks if they occur while the `inputEnabled` flag is `false`. + +We may get an error when making the request to the server, so the async operations must be surrounded with a `try/catch`. If an error occurs, we notify the user by displaying a message in the page, but we also log the error to the console. You may not want to log all errors to the console in a production application, but it is wise to do it when you are developing, so that you can find your own errors. + +First, we’ll make register and logon work. For either register or logon, if the step is successful, the back end returns a JWT token. This is stored for use in accessing jobs records. + +Adding these capabilities to `register.js` gives the following: + +```javascript +import { + inputEnabled, + setDiv, + message, + token, + enableInput, + setToken, +} from "./index.js"; +import { showLoginRegister } from "./loginRegister.js"; +import { showJobs } from "./jobs.js"; + +let registerDiv = null; +let name = null; +let email1 = null; +let password1 = null; +let password2 = null; + +export const handleRegister = () => { + registerDiv = document.getElementById("register-div"); + name = document.getElementById("name"); + email1 = document.getElementById("email1"); + password1 = document.getElementById("password1"); + password2 = document.getElementById("password2"); + const registerButton = document.getElementById("register-button"); + const registerCancel = document.getElementById("register-cancel"); + + registerDiv.addEventListener("click", async (e) => { + if (inputEnabled && e.target.nodeName === "BUTTON") { + if (e.target === registerButton) { + if (password1.value != password2.value) { + message.textContent = "The passwords entered do not match."; + } else { + enableInput(false); + + try { + const response = await fetch("/api/v1/auth/register", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: name.value, + email: email1.value, + password: password1.value, + }), + }); + + const data = await response.json(); + if (response.status === 201) { + message.textContent = `Registration successful. Welcome ${data.user.name}`; + setToken(data.token); + + name.value = ""; + email1.value = ""; + password1.value = ""; + password2.value = ""; + + showJobs(); + } else { + message.textContent = data.msg; + } + } catch (err) { + console.error(err); + message.textContent = "A communications error occurred."; + } + + enableInput(true); + } + } else if (e.target === registerCancel) { + name.value = ""; + email1.value = ""; + password1.value = ""; + password2.value = ""; + showLoginRegister(); + } + } + }); +}; + +export const showRegister = () => { + email1.value = null; + password1.value = null; + password2.value = null; + setDiv(registerDiv); +}; +``` + +Notice that we’ve now made the click event listener callback function `async`. We then check to see if the user entered matching passwords in the two inputs. If they did, disable clicking on the buttons and then make the `fetch()` call to the register endpoint. + +If the call is successful, we parse the data from the response, display a message to the user, and save the token to local storage using the function exported from `index.js`. We can then display the jobs page. If the call was not successful we will continue to show the register page and will display the error message from the response to the user. If any errors occur, then we catch them and continue to show the register page and show a generic error message to the user. + +After we’re done processing the call (whether it was successful or not) we re-enable clicking on the buttons. + +Notice that we always clear out the input values before we switch to another page. We do not want those to live on in memory. + +The `login.js` module becomes: + +```javascript +import { + inputEnabled, + setDiv, + token, + message, + enableInput, + setToken, +} from "./index.js"; +import { showLoginRegister } from "./loginRegister.js"; +import { showJobs } from "./jobs.js"; + +let loginDiv = null; +let email = null; +let password = null; + +export const handleLogin = () => { + loginDiv = document.getElementById("logon-div"); + email = document.getElementById("email"); + password = document.getElementById("password"); + const logonButton = document.getElementById("logon-button"); + const logonCancel = document.getElementById("logon-cancel"); + + loginDiv.addEventListener("click", async (e) => { + if (inputEnabled && e.target.nodeName === "BUTTON") { + if (e.target === logonButton) { + enableInput(false); + + try { + const response = await fetch("/api/v1/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: email.value, + password: password.value, + }), + }); + + const data = await response.json(); + if (response.status === 200) { + message.textContent = `Logon successful. Welcome ${data.user.name}`; + setToken(data.token); + + email.value = ""; + password.value = ""; + + showJobs(); + } else { + message.textContent = data.msg; + } + } catch (err) { + console.error(err); + message.textContent = "A communications error occurred."; + } + + enableInput(true); + } else if (e.target === logonCancel) { + email.value = ""; + password.value = ""; + showLoginRegister(); + } + } + }); +}; + +export const showLogin = () => { + email.value = null; + password.value = null; + setDiv(loginDiv); +}; +``` + +Make these changes and test the application again. You should find that you can register and logon. Logoff doesn’t work right at present, but this can be corrected in `jobs.js` with the following change: + +```javascript + } else if (e.target === logoff) { + setToken(null); + + message.textContent = "You have been logged off."; + + jobsTable.replaceChildren([jobsTableHeader]); + + showLoginRegister(); + } +``` + +Note that logoff involves no communication with the back end. The user is logged off by deleting the JWT from memory. We also have to clear the jobs data from memory, for security reasons, so that a non-logged-in user can’t see the previously logged-in user’s jobs. That’s what the replaceChildren does here: it replaces the contents of the `` element with just the `` element and nothing else. + +Next we need to make the changes so that we can create job entries. The `addEdit.js` module is changed as follows: + +```javascript +addEditDiv.addEventListener("click", async (e) => { + if (inputEnabled && e.target.nodeName === "BUTTON") { + if (e.target === addingJob) { + enableInput(false); + + let method = "POST"; + let url = "/api/v1/jobs"; + try { + const response = await fetch(url, { + method: method, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + company: company.value, + position: position.value, + status: status.value, + }), + }); + + const data = await response.json(); + if (response.status === 201) { + // 201 indicates a successful create + message.textContent = "The job entry was created."; + + company.value = ""; + position.value = ""; + status.value = "pending"; + + showJobs(); + } else { + message.textContent = data.msg; + } + } catch (err) { + console.log(err); + message.textContent = "A communication error occurred."; + } + + enableInput(true); + } else if (e.target === editCancel) { + message.textContent = ""; + showJobs(); + } + } +}); +``` + +In this case, we have to pass the JWT in the header for the call to work. + +Once you have added this code, try out the application again. You are now able to add entries, but you can’t actually see them. Next you add the code to populate the table of jobs entries. Of course, this involves another fetch operation, passing the JWT as for the add. Then the results are used to populate a table. + +There is a somewhat tricky part to this. We want to have edit and delete buttons for each row of the table. But, how do we associate an edit button with the edit operation, and when it is clicked, how do we know which entry is to be edited? This is done as follows. The edit buttons are given a class of `"editButton"`, and similarly, the delete buttons are given a class of `"deleteButton"`. We can also add an `data-` attribute to the buttons. These are called [data attributes](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) and in Javascript they correspond to a DOM hash/object called `dataset`. By adding a `data-id` attribute to the elemnet, we can access it via the `dataset.id` property. We want to set that id to be the id of that job (or your custom object), as returned from the database. + +It looks like this in `jobs.js`: + +```javascript +export const showJobs = async () => { + try { + enableInput(false); + + const response = await fetch("/api/v1/jobs", { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + const data = await response.json(); + let children = [jobsTableHeader]; + + if (response.status === 200) { + if (data.count === 0) { + jobsTable.replaceChildren(...children); // clear this for safety + } else { + for (let i = 0; i < data.jobs.length; i++) { + let rowEntry = document.createElement("tr"); + + let editButton = ``; + let deleteButton = ``; + let rowHTML = ` + + + +
${editButton}${deleteButton}
`; + + rowEntry.innerHTML = rowHTML; + children.push(rowEntry); + } + jobsTable.replaceChildren(...children); + } + } else { + message.textContent = data.msg; + } + } catch (err) { + console.log(err); + message.textContent = "A communication error occurred."; + } + enableInput(true); + setDiv(jobsDiv); +}; +``` + +So, plug this code into `jobs.js` at the appropriate point, and then try the application again. You should now be able to see the entries for each job. + +However, the edit and delete buttons don’t actually work. This is because the click handler in `jobs.js` isn’t set to look for them yet. We can add a section to the click handler to remedy this. + +```javascript + } else if (e.target.classList.contains("editButton")) { + message.textContent = ""; + showAddEdit(e.target.dataset.id); + } +``` + +The `dataset.id` contains the id of the entry to be edited. That is then passed on to the showAddEdit function. So we need to change that function to do something with this parameter. + +This function is in `addEdit.js`, and should be changed as follows: + +```javascript +export const showAddEdit = async (jobId) => { + if (!jobId) { + company.value = ""; + position.value = ""; + status.value = "pending"; + addingJob.textContent = "add"; + message.textContent = ""; + + setDiv(addEditDiv); + } else { + enableInput(false); + + try { + const response = await fetch(`/api/v1/jobs/${jobId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + const data = await response.json(); + if (response.status === 200) { + company.value = data.job.company; + position.value = data.job.position; + status.value = data.job.status; + addingJob.textContent = "update"; + message.textContent = ""; + addEditDiv.dataset.id = jobId; + + setDiv(addEditDiv); + } else { + // might happen if the list has been updated since last display + message.textContent = "The jobs entry was not found"; + showJobs(); + } + } catch (err) { + console.log(err); + message.textContent = "A communications error has occurred."; + showJobs(); + } + + enableInput(true); + } +}; +``` + +With this change, the `add/edit` div will be displayed with the appropriate values. If an add is being done, the function is called with a null parameter, and the form comes up blank with an add button. If an edit is being done, the function is called with the id of the entry to edit. The job is then retrieved from the database and the input fields are populated, and the button is changed to say update. We also store the id of the entry in the `dataset.id` of the `addEdit` div, so we keep track of which entry is to be updated. + +So far, so good, but what happens when the user clicks on the update button? In this case, we need to do a PATCH instead of a POST, and we need to include the id of the entry to be updated in the URL. So we need the following additional changes to addEdit.js: + +```javascript +if (e.target === addingJob) { + enableInput(false); + + let method = "POST"; + let url = "/api/v1/jobs"; + + if (addingJob.textContent === "update") { + method = "PATCH"; + url = `/api/v1/jobs/${addEditDiv.dataset.id}`; + } + + try { + const response = await fetch(url, { + method: method, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + company: company.value, + position: position.value, + status: status.value, + }), + }); + + const data = await response.json(); + if (response.status === 200 || response.status === 201) { + if (response.status === 200) { + // a 200 is expected for a successful update + message.textContent = "The job entry was updated."; + } else { + // a 201 is expected for a successful create + message.textContent = "The job entry was created."; + } + + company.value = ""; + position.value = ""; + status.value = "pending"; + showJobs(); + } else { + message.textContent = data.msg; + } + } catch (err) { + console.log(err); + message.textContent = "A communication error occurred."; + } + enableInput(true); +} +``` + +Make these changes, and editing jobs should work. Make sure that adding a new job still work correctly. + +This completes all CRUD operations except for delete. You will notice that the delete buttons don’t work yet. Fix this on your own by following the pattern used for the edit button. + +You’ll call the jobs delete API, and in the URL you will include the ID of the entry to be deleted. Then display the job list page if the delete was successful. + +**Note:** There is an error in the implementation of the delete operation in the jobs controller. The instructor’s guidance is to use this line: + +```javascript +res.status(StatusCodes.OK).send(); +``` + +This is incorrect, because an empty body is not valid JSON. Change it to: + +```javascript +res.status(StatusCodes.OK).json({ msg: "The entry was deleted." }); +``` + +If you do not make this change, an exception is thrown on the front end when you attempt to parse the response body with response.json(). + +You should be making commits as you go along. Once you have everything working, do a last `git add` and `git commit`, then push your `week12` branch to your Github repository. Then modify the Render.com deployment you have to point to the new branch. This will cause your new code to be deployed to Render.com. Verify that your application front end is working on Render.com. + +### Tips on Getting the Delete to Work + +* How do you know that the user wants to make a delete request? Each of the delete buttons is given a class of `deleteButton`. You check for that class in the e.target. +* How do you know which entry to delete? The id of the entry is stored in the `dataset.id` of the button. +* How do you do the delete? You need a call to `fetch` with a method of `DELETE` giving the URL of that entry. Be sure you include the JWT in the authorization header. Also, remember that fetch is asynchronous, and should be called in a `try/catch` block. +* What do you do if the delete succeeds? First, you put a message in the text content of the message paragraph. Second, you redraw the table showing the updated list of entries. The jobs.js module has a function for this. +* What do you do if the delete fails? Put a message indicating the failure in the message paragraph. +* Anything else? You don’t want to take input while these asynchronous operations are in progress, so you set the enabled flag before you start them, and clear it afterwards. \ No newline at end of file diff --git a/lessons/ctd-node-assignment-12.md b/lessons/ctd-node-assignment-12.md new file mode 100644 index 0000000000..39961e5c1e --- /dev/null +++ b/lessons/ctd-node-assignment-12.md @@ -0,0 +1,372 @@ +In this project (which continues for the next 3 lessons), you create a jobs application an alternate way from the previous session. This time, using server-side rendering with the `EJS` templating language. If you haven’t started your final project, you can use this project as the basis, using server-side rendering instead of the front-end + back-end model. Of course, to satisfy the requirements of the rubric, you would need to modify the schema to manage objects that are different from the jobs model. + +### Creating the Repository + +There is no starter repository for this project, so you will need to create one, with the following steps. First, create a `jobs-ejs` folder/directory on your computer (not in the tree of a previous project), and `cd` (change directories) into it. **Make sure that it is not in the tree of a previous project, that is, running `git status` _should_ return an error after you create the directory**. Next, create the `.gitignore` file and the `.env` file. The `.gitignore` file is critical to make sure your Mongo credentials and the Node libraries are not stored in Github. It should have these lines: + +``` +/node_modules +.env +.DS_Store +``` + +You can just copy the `.gitignore` file and the `.env` file from the `06-jobs-api` directory. You won’t use the JWT values from `.env`, so you can delete those. The `.env` file is needed for the Mongo credential, and eventually for the session secret. + +Next, run `git init` to make the directory a git repository. Then, log into Github and create a new repository called `jobs-ejs`. You create a new repository using the + button in the upper right of your Github screen. You do not use a template, the repository should be public, and you do not create a `README` or a `.gitignore`. + +![create new repo button](./images/create-new-repo.png) + +Once the repository has been created, you need to associate the Github repository with the repository on your laptop. You will see the following screen: + +![The github page for a newly created repository](./images/github-new.png) + +Click on the clipboard icon next to the URL to copy it. Then, in your laptop session for the jobs-ejs repository, type the following, where the URL is the one you copied to your clipboard + +``` +git remote add origin +git add -A +git commit -m "first commit" +git push origin main +``` + +The local repository is now associated with your Github repository. There isn’t much in it yet, just the `.gitignore`. Now create the `lesson12` branch, where you will do your work. + +### Components and Directories + +You need to initialize NPM for your repository. So, do an `npm init`. You can take all the defaults when prompted. This creates the `package.json`, and enables the installation of npm packages. You need to add `scripts` for dev and start to the `package.json` so that you can do `npm run` or `npm run dev`, where the `dev` script runs app.js using `nodemon` and the plain `npm run` runs it using `node`. + +You need to install the following packages: + +``` +bcryptjs +connect-flash +connect-mongodb-session +cookie-parser +dotenv +ejs +express +express-async-errors +express-rate-limit +express-session +helmet +host-csrf +mongoose +passport +passport-local +xss-clean +``` + +You should also install `eslint`, `prettier`, and `nodemon` as **dev dependencies** if you haven’t installed them globally. You’ve seen some of these packages before, but not others. Each will be explained as we use them. + +You will write to the Mongo database, so to save time, you can copy two directory trees from the `06-jobs-api` directory. You can use the following commands (you may have to adjust these depending on your directory structure / where you’ve set up your folders): + +``` +cp -r ../06-jobs-api/db . +cp -r ../06-jobs-api/models . +``` + +Now create the directory structure you will use, in particular the following directories: + +``` +controllers +routes +middleware +utils +views +views/partials +``` + +You do not need a `public` directory. The pages are rendered by the `EJS` engine from the `views` directory. + +Next, create the boilerplate `app.js`. It should look as follows: + +```javascript +const express = require("express"); +require("express-async-errors"); + +const app = express(); + +app.set("view engine", "ejs"); +app.use(require("body-parser").urlencoded({ extended: true })); + +// secret word handling +let secretWord = "syzygy"; +app.get("/secretWord", (req, res) => { + res.render("secretWord", { secretWord }); +}); +app.post("/secretWord", (req, res) => { + secretWord = req.body.secretWord; + res.redirect("/secretWord"); +}); + +app.use((req, res) => { + res.status(404).send(`That page (${req.url}) was not found.`); +}); + +app.use((err, req, res, next) => { + res.status(500).send(err.message); + console.log(err); +}); + +const port = process.env.PORT || 3000; + +const start = async () => { + try { + app.listen(port, () => + console.log(`Server is listening on port ${port}...`) + ); + } catch (error) { + console.log(error); + } +}; + +start(); +``` + +This boilerplate has crude page not found handling, as well as error handling. Those functions will be moved to middleware eventually. It also has something mysterious to handle the `/secretWord` route. So, if you run this app as is, it will return page not found for all URLs, except for `/secretWord`. That one returns an error — because we haven’t created the secretWord view! Once we have created the view, the `res.render` operation will display it. Note that we also need an `app.use` statement for the body parser. + +## First EJS file + +Create `views/secretWord.ejs`. The file should look as follows: + +``` + + + + + + + + Jobs List + + +

The secret word is: <%= secretWord %>

+

Would you like to change it?

+
+ +
+ + + + +``` + +This is an EJS file, but it looks just like HTML — except that section in `<%= %>`. Enclosed in those tags is JavaScript that is executed on the server side to modify the template. In this case, it just inserts the value of the `secretWord` variable. This value is passed to the EJS file via the second argument of the `res.render` function call in `app.js`. + +**Keep in mind** that the value must be a string or something that JavaScript knows how to convert to a string with a `toString()` method. If you try to pass in an object or some other complex data structure, you’ll likely just see it rendered as `[object Object]`. One way to handle that case would be to use the `JSON.stringify(object)` function to convert an object into a string. + +Note also that the `POST` operation does a `redirect`, telling the browser which URL should be displayed after processing is complete. This ends up calling `GET /secretWord` again, this time displaying the updated `secretWord` value. + +Try opening the URL. You should see that the secret word is displayed, and it can be changed. + +If the application has a lot of boilerplate, headers and footers and so on, we don’t want to have to duplicate that for every page. So we use “partials”. Create the following files: + +`views/partials/head.ejs` + +``` + + + + + + + Jobs List + + +``` + +`views/partials/header.ejs` + +``` +

The Jobs EJS Application

+
+``` + +`views/partials/footer.ejs` + +``` +
+

A copyright could go here.

+ + +``` + +Then change `views/secretWord.ejs` to substitute include statements, so that the whole thing reads: + +``` +<%- include("partials/head.ejs") %> +<%- include("partials/header.ejs") %> +

The secret word is: <%= secretWord %>

+

Would you like to change it?

+
+ +
+ + +<%- include("partials/footer.ejs") %> +``` + +You use the `<%- %>` to include HTML from other files. Be sure that you only use it with HTML that you trust, otherwise you could introduce a security exposure. Try the new page out. + +The data we are displaying is just the _value_ of the `secretWord` variable — but we could also insert data into the page that was retrieved from the database, as we’ll see. + +### Sessions + +There are a couple problems with the handling of the secret word. First, the value is stored globally — so every user sees the same value. We want the user to see only their own data. The second problem is that the data is stored in the memory of the server process, so when that server is restarted, the value is lost. We fix this using sessions. (Sessions may also be used with front-end+back-end applications.) + +Sessions are associated with a cookie, as we’ll see, and they are protected with a secret. So add a line to `.env` with this secret, as follows: + +``` +SESSION_SECRET=123lkawjg091u82378429 +``` + +The secret is some hard to guess string — and you **_never_** want to publicize it to Github! Then, add the following lines to `app.js`. These lines should be added _before_ any of the lines that govern routes, such as the `app.get` and `app.post` statements: + +```javascript +require("dotenv").config(); // to load the .env file into the process.env object +const session = require("express-session"); +app.use( + session({ + secret: process.env.SESSION_SECRET, + resave: false, + saveUninitialized: true, + }) +); +``` + +Change the logic so that the secret word is stored and retrieved in the session, as follows: + +```javascript +// let secretWord = "syzygy"; <-- comment this out or remove this line +app.get("/secretWord", (req, res) => { + if (!req.session.secretWord) { + req.session.secretWord = "syzygy"; + } + res.render("secretWord", { secretWord: req.session.secretWord }); +}); +app.post("/secretWord", (req, res) => { + req.session.secretWord = req.body.secretWord; + res.redirect("/secretWord"); +}); +``` + +`req.session` is an object that will persist across requests. + +Then try the URL. You will see that it works as before, except that if you have different sessions (from different browsers; or from a regular tab and an incognito tab) the value of the secretWord is different. However, if you restart the server, the value is lost. This is because while we’re no longer storing `secretWord` as a global variable, it is still being stored in the memory of the server. + +If you go into developer tools in your browser, and click on the `Application` tab, you can check that a cookie with the name of `connect.sid` has been associated with your browser session. + +![screenshot of Application tab for viewing cookies](./images/lesson12-view-cookies.png) + +This is the key used to retrieve session data. You can also see that the `HttpOnly` flag is set, so that browser-side code can’t access this cookie. + +We want to store the session data in a durable way. To do this, we’ll use Mongo as a session store. Replace the one line that does the `app.use` for session with all of these lines: + +```javascript +const MongoDBStore = require("connect-mongodb-session")(session); +const url = process.env.MONGO_URI; + +const store = new MongoDBStore({ + // may throw an error, which won't be caught + uri: url, + collection: "mySessions", +}); +store.on("error", function (error) { + console.log(error); +}); + +const sessionParms = { + secret: process.env.SESSION_SECRET, + resave: true, + saveUninitialized: true, + store: store, + cookie: { secure: false, sameSite: "strict" }, +}; + +if (app.get("env") === "production") { + app.set("trust proxy", 1); // trust first proxy + sessionParms.cookie.secure = true; // serve secure cookies +} + +app.use(session(sessionParms)); +``` + +These lines cause the session to be stored in Mongo. The bit about `sessionParms.cookie.secure = true` is important but may not be immediately obvious. It is saying that, if the application is running in production, the session cookie won’t work unless SSL is present. It’s a good policy, but as you are not running in production, you don’t have SSL. + +Then try the app again. Now you see that the `secretWord` value is preserved even if the server is restarted. If you go to your Mongo database, you can see the session data there — although it is not human-readable. + +### Flash Messages + +As the user performs operations, you need to inform them of the result. You do this with the `connect-flash` package. This stores the result of operations so that they can be subsequently displayed to the user. This information can’t be kept in server storage, because otherwise each user could see the others’ messages. And it can’t be stored in the req or res objects, because after an operation, there is typically a redirect, and the information would be lost. Therefore, the `connect-flash` package relies on the _session_. The package also keeps track of whether the message has already been displayed, so that it is only shown once. You can store multiple messages at different severity. + +Add the following code. Note that this code must come after the `app.use` that sets up sessions, because flash depends on sessions: + +``` +app.use(require("connect-flash")()); +``` + +We want to set some messages into flash. To do this, change the `POST` route for `/secretWord` to look like this: + +```javascript +app.post("/secretWord", (req, res) => { + if (req.body.secretWord.toUpperCase()[0] == "P") { + req.flash("error", "That word won't work!"); + req.flash("error", "You can't use words that start with p."); + } else { + req.session.secretWord = req.body.secretWord; + req.flash("info", "The secret word was changed."); + } + res.redirect("/secretWord"); +}); +``` + +These messages should be displayed on the next screen. Note that you can have multiple info or error messages. In order for them to be displayed, we need to add code in the view, in the `header.js` partial template, as follows: + +``` +

The Jobs EJS Application

+<% if (errors) { + errors.forEach((err) => { %> +
Error: <%= err %>
+<% }) } %> +<% if (info) { + info.forEach((msg) => { %> +
Info: <%= msg %>
+<% }) } %> +
+``` + +Whoa! you may be saying. That doesn’t look like HTML! What will the browser do with it? The answer is that the browser never sees this stuff. The things in `<% %>` are JavaScript, executed on the server side, and the render process removes this and replaces it with the result of the code. There is logic, which is executed but not displayed, in the `<% %>` parts. Then, there is substitution of values, in the `<%= %>` parts, where the equals sign indicates that a value is to be displayed. So, this code checks the `info` and `errors` arrays, displaying values from them if any are present. + +But, the problem is that the `info` and `errors` arrays need to get passed into the EJS file, when the render is called. This could be done as follows: + +```javascript +res.render("secretWord", { + secretWord, + errors: flash("error"), + info: flash("info"), +}); +``` + +But this is a little clumsy, because if we have a bunch of pages we render, every render statement would have to be modified. So, instead, we put the values in `res.locals`. That hash contains values that are always available to the EJS rendering engine. As follows: + +```javascript +app.get("/secretWord", (req, res) => { + if (!req.session.secretWord) { + req.session.secretWord = "syzygy"; + } + res.locals.info = req.flash("info"); + res.locals.errors = req.flash("error"); + res.render("secretWord", { secretWord: req.session.secretWord }); +}); +``` + +This is how `res.locals` is loaded with the right stuff. However, we’d want to move the `res.locals` statements into a middleware routine that always runs (_after_ the flash middleware, but _before_ any of the routes), and we’ll do that eventually. + +### Submitting Your Work + +To submit your work, you add, commit, and push your branch as usual, create a pull request and include a link to your pull request in the homework submission. In the next lesson, we will implement authentication using Passport, and in the final lesson, we’ll manage Jobs entries in the database. \ No newline at end of file diff --git a/lessons/ctd-node-assignment-13.md b/lessons/ctd-node-assignment-13.md new file mode 100644 index 0000000000..ac60cab4a6 --- /dev/null +++ b/lessons/ctd-node-assignment-13.md @@ -0,0 +1,467 @@ +In this lesson, you use the `passport` and `passport-local` npm packages to handle user authentication, from within a server-side rendered application. + +### First Steps + +You continue to work with the same repository as the previous lesson, but you create a new branch called `lesson13`. + +The user records are stored in the Mongo database, just as for the Jobs API lesson. You have already copied the `models` directory tree from the Jobs API lesson into the `jobs-ejs` repository. The user model is used unchanged. You configure passport to use that model. + +To begin, you will need the following views: + +``` +views/index.ejs +views/logon.ejs +views/register.ejs +``` + +The `index.ejs` view just shows links to login or register. The `logon.ejs` view collects the email and password from the user. The register collects the name, email, and password for a new user. We want to use the partials as well. We want to modify the header partial to give the name of the logged on user, and to add a logoff button if a user is logged on. + +`views/index.ejs`: + +``` +<%- include("partials/head.ejs") %> +<%- include("partials/header.ejs") %> +<% if (user) { %> + Click this link to view/change the secret word. +<% } else { %> + Click this link to logon. + Click this link to register. +<% } %> +<%- include("partials/footer.ejs") %> +``` + +`views/logon.ejs`: + +``` +<%- include("partials/head.ejs") %> +<%- include("partials/header.ejs") %> +
+
+ + +
+
+ + +
+
+ + + + +
+ +<%- include("partials/footer.ejs") %> +``` + +`views/register.js` + +``` +<%- include("partials/head.ejs") %> +<%- include("partials/header.ejs") %> +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+ +<%- include("partials/footer.ejs") %> +``` + +Revised `views/partials/header.ejs`: + +``` +

The Jobs EJS Application

+<% if (user) { %> +

User <%= user.name %> is logged on.

+
+ + +<% } %> +<% if (errors) { + errors.forEach((err) => { %> +
+ Error: <%= err %> +
+ <% }) + } %> + <% if (info) { + info.forEach((msg) => { %> +
+ Info: <%= msg %> +
+ <% }) + } %> +
+``` + +These changes won’t suffice to do anything in the application, until routes are added to match. We need to follow best practices, with separate route and controller files. + +### Router and Controller + +Create a file `routes/sessionRoutes.js`, as follows: + +```javascript +const express = require("express"); +// const passport = require("passport"); +const router = express.Router(); + +const { + logonShow, + registerShow, + registerDo, + logoff, +} = require("../controllers/sessionController"); + +router.route("/register").get(registerShow).post(registerDo); +router + .route("/logon") + .get(logonShow) + .post( + // passport.authenticate("local", { + // successRedirect: "/", + // failureRedirect: "/sessions/logon", + // failureFlash: true, + // }) + (req, res) => { + res.send("Not yet implemented."); + } + ); +router.route("/logoff").post(logoff); + +module.exports = router; +``` + +Ignore the passport lines for the moment. This just sets up the routes. We need to create a corresponding file `controllers/sessionController.js`. Here we use the `User` model. However, the file you copied makes some references to the JWT library. You must edit `models/User.js` to remove those references in order for `User.js` to load. We aren’t using JWTs in this project. + +```javascript +const User = require("../models/User"); +const parseVErr = require("../util/parseValidationErr"); + +const registerShow = (req, res) => { + res.render("register"); +}; + +const registerDo = async (req, res, next) => { + if (req.body.password != req.body.password1) { + req.flash("error", "The passwords entered do not match."); + return res.render("register", { errors: flash("errors") }); + } + try { + await User.create(req.body); + } catch (e) { + if (e.constructor.name === "ValidationError") { + parseVErr(e, req); + } else if (e.name === "MongoServerError" && e.code === 11000) { + req.flash("error", "That email address is already registered."); + } else { + return next(e); + } + return res.render("register", { errors: flash("errors") }); + } + res.redirect("/"); +}; + +const logoff = (req, res) => { + req.session.destroy(function (err) { + if (err) { + console.log(err); + } + res.redirect("/"); + }); +}; + +const logonShow = (req, res) => { + if (req.user) { + return res.redirect("/"); + } + res.render("logon", { + errors: req.flash("error"), + info: req.flash("info"), + }); +}; + +module.exports = { + registerShow, + registerDo, + logoff, + logonShow, +}; +``` + +We have two endpoints that handle just rendering an EJS page, `registerShow` and `logonShow`. + +We then have endpoints for actually performing the logoff and register actions. We don’t need a controller handler for login, because Passport handles that for us. + +The `registerDo` handler will check if the two passwords the user entered match, and refresh the page otherwise. If all is good there, it will create a user in the database and redirect to the home page. The creation of the user entry in Mongo is just the same as it was for the Jobs API. We also have some error handling cases here. + +If there is a Mongoose validation error when creating a user record, we need to parse the validation error object to return the issues to the user in a more helpful format, and we do that in the file util/parseValidationErrs.js: + +```javascript +const parseValidationErrors = (e, req) => { + const keys = Object.keys(e.errors); + keys.forEach((key) => { + req.flash("error", key + ": " + e.errors[key].properties.message); + }); +}; + +module.exports = parseValidationErrors; +``` + +We need some middleware to load `res.locals` with any variables we need, like the logged in user and flash properties. Create `middleware/storeLocals.js`: + +```javascript +const storeLocals = (req, res, next) => { + if (req.user) { + res.locals.user = req.user; + } else { + res.locals.user = null; + } + res.locals.info = req.flash("info"); + res.locals.errors = req.flash("error"); + next(); +}; + +module.exports = storeLocals; +``` + +Now, we need a couple of `app.use` statements. Add these lines right after the `connect-flash` line: + +```javascript +app.use(require("./middleware/storeLocals")); +app.get("/", (req, res) => { + res.render("index"); +}); +app.use("/sessions", require("./routes/sessionRoutes")); +``` + +The storeLocals middleware sets the values for errors, info, and user, but in registerDo above, we have to pass the value for errors on the render call, because we changed the value of the flash errors after storeLocals ran. + +We are now using the database. So, we need to connect to it at startup. You need a file, `db/connect.js`. Check that it looks like the following: + +```javascript +const mongoose = require("mongoose"); + +const connectDB = (url) => { + return mongoose.connect(url, {}); +}; + +module.exports = connectDB; +``` + +Then add this line to `app.js`, just before the listen line: + +```javascript +await require("./db/connect")(process.env.MONGO_URI); +``` + +Then try the application out, starting at the `"/"` URL. You can try each of the new views. But you still can’t logon. The logon operation is commented out, because Passport is not set up. + +### Configuring Passport + +To use Passport, you have to tell it how to authenticate users, retrieving them from the database. Create a file `passport/passportInit.js`, as follows: + +```javascript +const passport = require("passport"); +const LocalStrategy = require("passport-local").Strategy; +const User = require("../models/User"); + +const passportInit = () => { + passport.use( + "local", + new LocalStrategy( + { usernameField: "email", passwordField: "password" }, + async (email, password, done) => { + try { + const user = await User.findOne({ email: email }); + if (!user) { + return done(null, false, { message: "Incorrect credentials." }); + } + + const result = await user.comparePassword(password); + if (result) { + return done(null, user); + } else { + return done(null, false, { message: "Incorrect credentials." }); + } + } catch (e) { + return done(e); + } + } + ) + ); + + passport.serializeUser(async function (user, done) { + done(null, user.id); + }); + + passport.deserializeUser(async function (id, done) { + try { + const user = await User.findById(id); + if (!user) { + return done(new Error("user not found")); + } + return done(null, user); + } catch (e) { + done(e); + } + }); +}; + +module.exports = passportInit; +``` + +Here we are registering a “strategy” to tell Passport how to handle requests that we pass to it `passport.use` is a function that takes the name you want to give this passport strategy (here we’re using `"local"`) and the strategy as the second argument, where we’re passing in a `new LocalStrategy` from the `passport-local` library. + +The `LocalStrategy` constructor takes two arguments: an options object, and a callback function. The options object expects us to tell the strategy what field on the request will have the user identifier, and what field will have the password. Here, we tell it to look for `"email"` as the identifier, and `password` for the password. + +The second argument passed to the `LocalStrategy` constructor is the callback function that will be called when we do `passport.authenticate(...)` in the login route in `sessionRoutes.js`. The `local-strategy` library will extract the `email` and `password` fields that we defined in the options/first argument, from the `req.body`. If it doesn’t find those fields, it will return a 400 error. If it does find them, then it will then pass those (along with a `done` callback) to the function we’ve defined above. It’s up to us then to write the code specific to our application, that handles checking if the user exists and has used to correct password. + +The `done` callback accepts three arguments: + +1. An error, if there is one, otherwise just pass in `null` +2. The user object, if the user is found and used valid credentials, otherwise `false` +3. An object with a `message` property and `type`, this can be used in both error and success cases to show flash messages. Here we’e only using it in non-error failure cases (user not found or wrong credentials used). + +In addition to registering our local strategy, we also define `serializeUser` and `deserializeUser` functions on the `passport` object. + +`serializeUser` is used when saving a user to the request session object. We can’t save an object to the session cookie, so we need to “serialize” it a.k.a. encode it as a string. In the above function, we’re doing that by telling Passport to save just the user id to the session cookie. + +Then when Passport is handling a protected route, it will use the `deserializeUser` function to retrieve the user from the database, using the id that was saved to the session cookie. + +You can now add the following lines to `app.js`, right _after_ the `app.use` for session (Passport relies on session): + +```javascript +const passport = require("passport"); +const passportInit = require("./passport/passportInit"); + +passportInit(); +app.use(passport.initialize()); +app.use(passport.session()); +``` + +First we call the `passportInit` function that we created in the previous section. This registers our `local` Passport strategy, and sets up the `serializeUser` and `deserializeUser` functions onto the `passport` object. + +Then we call `passport.initialize()` (which sets up Passport to work with Express and sessions) and `passport.session()` (which sets up an Express middleware that runs on all requests, checks the session cookie for a user id, and if it finds one, deserializes and attaches it to the `req.user` property). + +Finally, you can now uncomment the lines having to do with Passport in `routes/sessionRoutes.js`, so that the require statement for Passport is included, and so that the route for logon looks like + +```javascript +router + .route("/logon") + .get(logonShow) + .post( + passport.authenticate("local", { + successRedirect: "/", + failureRedirect: "/sessions/logon", + failureFlash: true, + }) + ); +``` + +This means that when someone sends a `POST` request to the `/sessions/logon` path, Passport will call the function we defined earlier and registered to the name `"local"`. If that function completes successfully (`done(...)` is called with no error argument), then it will redirect the page to the `successRedirect` page. If there is an error, then it will redirect to the `failureRedirect` page, and also set a flash message with the `message` property from the object we passed to `done(...)`. + +Since we’re letting Passport handle setting the `req.flash` properties now, we can remove the lines in `controllers/sessionController.js` that set the flash messages for the `loginShow` handler. So that should now just look like: + +```javascript +const logonShow = (req, res) => { + if (req.user) { + return res.redirect("/"); + } + res.render("logon"); +}; +``` + +After that, try logon for one of the accounts you have created. You should see that you are logged in and can access the `secretWord` page. You should also see appropriate error messages for bad logon credentials. Also, test logoff. + +### Protecting a Route + +To protect a route, you need some middleware, as follows. + +`middleware/auth.js`: + +```javascript +const authMiddleware = (req, res, next) => { + if (!req.user) { + req.flash("error", "You can't access that page before logon."); + res.redirect("/"); + } else { + next(); + } +}; + +module.exports = authMiddleware; +``` + +`req.user` is injected into the `req` object by `passport.session()`. We can check if it exists (a.k.a. if the user is logged-in) in this middleware and use that to determine if the non-logged-in requester should be redirected to the home page, or if the logged-in user should be allowed to continue to the next middleware or controller handler function. + +We want to protect any route for the `"/secretWord"` path. The best practice is to put the code for those routes into a router file, as follows. + +`routes/secretWord.js`: + +```javascript +const express = require("express"); +const router = express.Router(); + +router.get("/", (req, res) => { + if (!req.session.secretWord) { + req.session.secretWord = "syzygy"; + } + + res.render("secretWord", { secretWord: req.session.secretWord }); +}); + +router.post("/", (req, res) => { + if (req.body.secretWord.toUpperCase()[0] == "P") { + req.flash("error", "That word won't work!"); + req.flash("error", "You can't use words that start with p."); + } else { + req.session.secretWord = req.body.secretWord; + req.flash("info", "The secret word was changed."); + } + + res.redirect("/secretWord"); +}); + +module.exports = router; +``` + +We could further refactor this by moving the code for handling the routes (`(req, res) => {...}`) a controller file, like we’ve done for the session routes, but we’ll leave it like this for now. + +Next let’s replace the `app.get` and `app.post` statements for the `"/secretWord"` routes in `app.js` with these lines: + +```javascript +const secretWordRouter = require("./routes/secretWord"); +app.use("/secretWord", secretWordRouter); +``` + +Then try out the secretWord page to make sure it still works. Turning on protection is simple. You add the authentication middleware to the route above as follows: + +```javascript +const auth = require("./middleware/auth"); +app.use("/secretWord", auth, secretWordRouter); +``` + +That causes the authentication middleware to run before the `secretWordRouter`, and it redirects if any requests are made for those routes before logon. Try it out: login and verify that you can see and change the secretWord. Then log off and try to go to the `"/secretWord"` URL. + +### Submitting Your Work + +As usual, add and commit your changes and push the `lesson13` branch to your Github. Then create the pull request and include the link in your homework submission. \ No newline at end of file diff --git a/lessons/ctd-node-assignment-14.md b/lessons/ctd-node-assignment-14.md new file mode 100644 index 0000000000..8e21276f0a --- /dev/null +++ b/lessons/ctd-node-assignment-14.md @@ -0,0 +1,125 @@ +You continue to work in the `jobs-ejs` repository. Create a branch called `lesson14` for this week’s work. + +### Fixing the Security + +Passport is using the session cookie to determine if the user is logged in. This creates a security vulnerability called “cross site request forgery” (CSRF). We will demonstrate this. + +To see this, clone **[this repository](https://github.com/Code-the-Dream-School/csrf-attack)** into a separate directory, outside of the current `jobs-ejs` folder. Then, within the directory you cloned, install packages with `npm install` and run the app with `node app`. This will start another express application listening on port **4000** of your local machine. This is the attacking code. It could be running anywhere on the Internet — that has nothing to do with the attack. + +You should have two browser tabs open, one for localhost:3000, and one for localhost:4000\. The one at localhost:4000 just shows a button that says Click Me! **Don’t click it yet**. Use the `jobs-ejs` application in the 3000 tab to set the secret string to some value. Then close the tab for localhost:3000\. Then open a new tab for localhost:3000\. Then check the value of the secret string. So far so good — it still has the value you set. If you log off, your session is discarded. Try this: Log off. Then click the button in the localhost:4000 tab. Then log back on and view the secret string. It is back to syzygy. Set it to a custom value. + +Now, without logging off of jobs-ejs , click the button in the 4000 tab. Then refresh the /secretWord page in `jobs-ejs`. Hey, what happened! (By the way, this attack would succeed even if you closed the 3000 tab entirely.) + +You see, the other application sends a request to your application in the context of your browser — and that browser request automatically includes the cookie. So, the application thinks the request comes from a logged on user, and honors it. If the application, as a result of a form post, makes database changes, or even transfers money, the attacker could do that as well. + +So, how to fix this? This is the purpose of the host-csrf package you installed at the start of the project. Follow the instructions **[here](https://www.npmjs.com/package/host-csrf#:~:text=The%20csrf%20middleware,Example%3A)** to integrate the package with your application. You will need to change app.js as well as **each of the forms** in your EJS files. You can use `process.env.SESSION_SECRET` as your `cookie-parser` secret. Note that the `app.use` for the CSRF middleware must come _after_ the cookie parser middleware and _after_ the body parser middleware, but _before_ any of the routes. You will see a message logged to the console that the CSRF protection is not secure. That is because you are using HTTP, not HTTPS, so the package is less secure in this case, but you would be using HTTPS in production. As you will see, it stops the attack. + +Re-test, first to see that your application still works, and second, to see that the attack no longer works. (A moral: Always log off of sensitive applications before you surf, in case the sensitive application is vulnerable in this way. Also note that it does not help to close the application, as the cookie is still present in the browser. You have to log off to clear the cookie. Even restarting the browser does not suffice.) + +Enabling CSRF protection in the project is an _important_ part of this lesson — don’t omit it! By the way, the CSRF attack only works when the credential is in a cookie. It doesn’t work if you use JWTs in the authorization header. However, as we've seen, to send JWTs in an authorization header, you have to store sensitive data in browser local storage, which is always a bad idea. + +### A Couple of Tips + +The rest of this lesson shows how to build a dynamic database application with **no client-side JavaScript.** Of course, in real-world applications, you’ll often have client side JavaScript, but this lesson shows that you can do a lot of things without it. + +However, it does necessitate some differences in approach. If all you have on the client side is HTML, the client can only send `GET` requests (for links) or `POST` requests (for submitting a form). You can’t send `PUT`, `PATCH`, or `DELETE` operations from HTML — unless you add in some client-side JavaScript. So, in this lesson, all routes are GET and POST routes. + +You are going to get a list of job listings and display them in a table. You are also going to enable the user to create a new job listing, edit an existing one, or delete one from the list. As always, a given user can only access the entries they own, and not other people’s. Because you can’t do `PUT`, `PATCH`, or `DELETE`, you’ll do `POST` operations for each of these, giving a different URL for each so that the server knows what to do. **Never add, update, or delete data using a `GET`.** That would introduce security vulnerabilities. + +Your table should have columns for each the attributes (company, position, status) of each job listing. In addition, it should have buttons on each row to edit or delete an entry. Editing an entry starts with a `GET` to display a form. Deleting an entry just sends a `POST`. So, you should have routes something like this: + +``` +GET /jobs (display all the job listings belonging to this user) +POST /jobs (Add a new job listing) +GET /jobs/new (Put up the form to create a new entry) +GET /jobs/edit/:id (Get a particular entry and show it in the edit box) +POST /jobs/update/:id (Update a particular entry) +POST /jobs/delete/:id (Delete an entry) +``` + +In your table, you’ll have a button for edit and a button for delete. The button for edit should do a GET, so that’s a link. A good way to make a link look like a button is to put the button inside the link, as follows: + +``` + + + +``` + +This is really a form masquerading as a button. And, because it’s a form, you have to add the `_csrf` token, or your CSRF protection won’t let the operation through. The `display: inline` allows this to line up on the table row. + +Ok, so how to build the table? The `GET` for `"/jobs"` comes in, and your router calls a function in your controller to pull all the job listings for that user from the database into a jobs array (which might be empty). Then the controller function makes the following call: + +```javascript +res.render("jobs", { jobs }); +``` + +This render call is going to load and parse` /views/jobs.ejs`, passing the array as a local variable to that template. Now you need to construct the table, using EJS code. It will look something like this: + +``` +

Jobs List

+
${data.jobs[i].company}${data.jobs[i].position}${data.jobs[i].status}
+ + + + + + + <% if (jobs && jobs.length) { %> + <% jobs.forEach((job) => { %> + + + + + + + + <% }) %> + <% } %> +
CompanyPositionStatus
<%= job.company %><%= job.position %><%= job.status %>button type="button">delete
+``` + +Of course, you also have `include` statements for the header and footer in this ejs file. You see the conditional JavaScript logic in the EJS brackets `<% %>.` But, the buttons aren’t going to do anything yet. So you need to substitute this for the edit button: + +``` + + + +``` + +That puts the actual id of the job listing into the URL. Similarly, for the delete button, you have to build one of those button-only forms described above, and it should have the following as its action attribute: + +``` +action="/jobs/delete/<%= job.id %>" +``` + +So that the actual id of the entry to delete is included in the URL on the `POST`. Enough on the tips. Here are the steps to complete the project. + +### Steps + +1. Create `routes/jobs.js` and `controllers/jobs.js`. The router should have each of the routes previously described, and the controller should have functions to call when each route is invoked. Remember that `req.params` will have the id of the entry to be edited, updated, or deleted. You might want to start with simple `res.send()` operations to make sure each of the routes and controller functions are getting called as expected. +2. In `app.js`, `require` the jobs router, and add an `app.use` statement for it, at an appropriate place in the code. The `app.use` statement might look like: + +```javascript +app.use("/jobs", auth, jobs); +``` + +You need to include the auth middleware in the `app.use`, because these are protected routes and the requester must be a logged on user. + +1. Test your routes. You can test the `GET` routes from the browser. For the `POST` routes, you’ll need to use Postman. +2. Create `views/jobs.ejs`. That should have the table described above, plus a button to add a new entry. +3. Create `views/job.ejs` (note the singular form here rather than plural like in step 4). That should have the fields so that you can create an entry. You’ll want to use the same form for adding and editing. When adding, you’ll do `res.render("job", { job: null })`. That will tell `job.ejs` that it is doing an add because there’s no value in the `job` local variable. When editing, you’ll do `res.render("job", { job })`. When a non-null entry is passed to `job.ejs`, then the form knows it is doing an edit, so the fields are populated and the button says update. Note that the action for the form is different for each case. If job is null, then `action="/job"`. But if job is not null, then `action="/job/update/<%= job.id %>` so that the update route is called. +4. Add the necessary Mongo calls to `controllers/jobs.js`. You first require the `models/Job` model, so that you can do `Job.create`, `Job.findOne`, etc. As always, have appropriate error handling. You can use `util/parseValidationErrs.js` to handle validation errors. You can use `flash` to pass error and information messages to the user. Be sure that if you do an edit or update or delete for an entry, that that entry belongs to the active user. Here’s a hint: Passport stores the active user in `req.user`. So you can use `req.user._id` for your `createdBy` value. +5. Add a link to `index.html` for the `/jobs` URL. +6. Try it out! +7. There is one more step. You need to make your application more secure! You should configure the helmet, xss-clean, and express-rate-limit packages, just as you did for Lesson 10\. Then try the application out one more time. CORS is not needed in this case. + +### Submitting Your Work + +The usual steps apply. \ No newline at end of file diff --git a/lessons/ctd-node-assignment-15.md b/lessons/ctd-node-assignment-15.md new file mode 100644 index 0000000000..7833548d2f --- /dev/null +++ b/lessons/ctd-node-assignment-15.md @@ -0,0 +1,535 @@ +You continue with the same jobs-ejs repository from your previous lesson, but create a new branch called lesson 15. + +The instructions below are for the Job model. If you are using a different or modified model, so as to prepare for your final project, you will have to adjust the code below. + +The first step is to install the packages you will need. These are development dependencies -- you do not need them in your runtime, if you deploy this application to the internet -- so you install them with the ```--save-dev``` flag. You need mocha, chai, puppeteer, @faker-js/faker, and factory-bot, as follows: +``` +npm install --save-dev mocha +npm install --save-dev chai +npm install --save-dev chai-http +npm install --save-dev factory-bot +npm install --save-dev @faker-js/faker +npm install --save-dev puppeteer +``` +A suggestion: You probably should update the connect-mongodb-session package. There have been some serious security bugs in that package, now fixed. When you have completed the above npm install operations, check your package.json. In the devDependencies stanza you should have entries for the packages above. Verify that your chai and chai-http entries are for some level of version 5 of those packages, as the instructions below are specific to version 5. + +## Setting Up To Test + +Create a test directory in your repository. This is where you will put the actual test cases. Edit your +.env file. Currently you have a line for MONGO_URI. Duplicate the line, and then change the copy to MONGO_URI_TEST. Add "-test" onto the end of the value. This gives you a separate test database. Edit your package.json. In the scripts stanza, the line for "test" should be changed to read: +``` + "test": "NODE_ENV=test mocha tests/*.js --timeout 5000 --exit", +``` +which will cause the tests to run. It also sets the NODE_ENV environment variable, which we'll use to load the test version of the database. Edit your app.js. You'll have a line that reads something like: +```javascript + const url = process.env.MONGO_URI; +``` +You should change it to look something like the following: +```javascript +let mongoURL = process.env.MONGO_URI; +if (process.env.NODE_ENV == "test") { + mongoURL = process.env.MONGO_URI_TEST; +} +``` +and then change url to mongoURL in the section that starts ```const store = ```. +The point of this is so that your testing doesn't interfere with your production database, and also so that your production or development data doesn't interfere with your testing. Also, you want to have a function that will bring the database to a known state, so that previous tests don't cause subsequent ones to give false results. Create a file util/seed_db.js. It should read as follows: +```javascript +const Job = require("../models/Job"); +const User = require("../models/User"); +const faker = require("@faker-js/faker").fakerEN_US; +const FactoryBot = require("factory-bot"); +require("dotenv").config(); + +const testUserPassword = faker.internet.password(); +const factory = FactoryBot.factory; +const factoryAdapter = new FactoryBot.MongooseAdapter(); +factory.setAdapter(factoryAdapter); +factory.define("job", Job, { + company: () => faker.company.name(), + position: () => faker.person.jobTitle(), + status: () => + ["interview", "declined", "pending"][Math.floor(3 * Math.random())], // random one of these +}); +factory.define("user", User, { + name: () => faker.person.fullName(), + email: () => faker.internet.email(), + password: () => faker.internet.password(), +}); + +const seed_db = async () => { + let testUser = null; + try { + const mongoURL = process.env.MONGO_URI_TEST; + await Job.deleteMany({}); // deletes all job records + await User.deleteMany({}); // and all the users + testUser = await factory.create("user", { password: testUserPassword }); + await factory.createMany("job", 20, { createdBy: testUser._id }); // put 30 job entries in the database. + } catch (e) { + console.log("database error"); + console.log(e.message); + throw e; + } + return testUser; +}; + +module.exports = { testUserPassword, factory, seed_db }; +``` +A couple of new ideas are introduced above. First, faker is being used to generate somewhat random but plausible data. Second, we are using factories to automate the creation of data, which is being written to the database. + +## Chai 5 and Chai-Http 5 + +These packages are now ESM only! This was, in my humble opinion, a questionable move on the part of the developers, and they made quite a few other breaking changes. But we can accommodate these changes, without converting to ESM modules. (Some students are using ESM modules for these exercises. If you are doing this, discuss matters with your mentors if you have trouble.) + +For Chai 4 and Chai-http 4, we could do: +```javascript +const chai = require('chai') +const chaiHttp = require('chai-http') +chai.use(chaiHttp) +``` +This would give you access to chai.expect() (for evaluating results) and chai.request() (for sending http requests to the server and getting back the results). This is not going to work for V5 of these packages: You can't use request() to load an ESM only module. Also you can only call chai.use() once, for all of your test files and cases. So, we need the following utility module, util\get_chai.js: +```javascript +let chai_obj = null; + +const get_chai = async () => { + if (!chai_obj) { + const { expect, use } = await import("chai"); + const chaiHttp = await import("chai-http"); + const chai = use(chaiHttp.default); + chai_obj = { expect: expect, request: chai.request }; + } + return chai_obj; +}; + +module.exports = get_chai; +``` +In this way, we avoid using request(), and we can ensure that chai.use() is only called once. But, get_chai() is asynchronous. When we use Mocha, we can't call an asynchronous function in the mainline of a test file, because mocha won't wait for the promise to resolve. Also, describe() functions can't be passed asynchronous functions. We can and should pass asynchronous functions to it() functions, for the individual tests. So, this is where we call get_chai(), inside each asynchronous function passed to it(). + +(Some students may be using EJS files. This is somewhat easier, in that you can use the import statement instead of the import() asynchronous function. However, you still need a utility module to ensure that use() is only called once.) + +## Unit Testing a Function + +Create a file, utils/multiply.js. It should export a function, multiply(), that takes two arguments and returns the product. Now we can write a unit test, in tests/test_multipy.rb: +```javascript +const multiply = require("../util/multiply"); +const get_chai = require("../util/get_chai"); + +describe("testing multiply", () => { + it("should give 7*6 is 42", async () => { + const { expect } = await get_chai(); + expect(multiply(7, 6)).to.equal(42); + }); + it('should give 7*6 is 97', async () => { + const {expect} = await get_chai(); + expect(multiply(7,6)).to.equal(97); + }); +}); +``` +Here we get the value for expect() several times. By default, the test cases run in order, so one could store the value in a variable with module scope, and only get it once per test file ... but one can run tests in parallel, in which case things would probably not work. + +Then do: ```npm run test``` You will see that the first test passes, but the second one fails, as one would think. You can delete the second test. You might want to create tests for other numbers, to make sure the function doesn't always return 42. + +## Function Testing for An API + +Your current application doesn't have an API, so you can add one by adding the following, at an appropriate place (like before the not found handler) to app.js: +```javascript +app.get("/multiply", (req, res) => { + const result = req.query.first * req.query.second; + if (result.isNaN) { + result = "NaN"; + } else if (result == null) { + result = "null"; + } + res.json({ result: result }); +}); +``` +You also have to change app.js to make your app available to the test. The bottom of the file should look like: +```javascript +const port = process.env.PORT || 3000; +const start = () => { + try { + require("./db/connect")(mongoURL); + return app.listen(port, () => + console.log(`Server is listening on port ${port}...`), + ); + } catch (error) { + console.log(error); + } +}; + +start(); + +module.exports = { app }; +``` +Here, to facilitate testing, we have made start() synchronous. You can try the multiply API out if you like, by starting the server and doing the following in your browser: +``` +http://localhost:3000/multiply?first=5&second=27 +``` +Then create a test, a file tests/test_multiply_api.js, as follows: +```javascript +const { app } = require("../app"); +const get_chai = require("../util/get_chai"); + +describe("test multiply api", function () { + it("should multiply two numbers", async () => { + const { expect, request } = await get_chai(); + const req = request + .execute(app) + .get("/multiply") + .query({ first: 7, second: 6 }) + .send(); + const res = await req; + expect(res).to.have.status(200); + expect(res).to.have.property("body"); + expect(res.body).to.have.property("result"); + expect(res.body.result).to.equal(42); + }); +}); +``` +Note first of all that this file actually requires your app, which causes your app to run. You do not want your server running when you run the test, because the require() function for the app starts it. Chai is going to send data to that running app. The chai-http package adds HTTP functions to Chai, so it now has the get() method (as well as post, patch, etc.), and these return a request object with methods query and send. One can then check the result status and body. Do ```npm run test``` to try it out. + +## Function Testing for Rendered HTML + +Of course, the application you are writing is not intended to provide an API. Instead it provides rendered HTML pages. You can test these as well. + +There are two annoying problems to deal with, one in Chai and one in the Express rendering engine. In Express, when a page is rendered, it should set the Content-Type response header to be text/html. But it doesn't. The second problem is that if Chai receives a response without the Content-Type header, it tries to parse it as JSON, and throws an error if that fails. It should catch the error, but it doesn't, which is crude. You can fix the issue by setting the Content-Type header appropriately with this middleware, which should be added to app.js before your routes: +```javascript +app.use((req, res, next) => { + if (req.path == "/multiply") { + res.set("Content-Type", "application/json"); + } else { + res.set("Content-Type", "text/html"); + } + next(); +}); +``` +Now create a simple UI test case, in tests/test_ui.js: +```javascript +const { app } = require("../app"); +const get_chai = require("../util/get_chai"); + +describe("test getting a page", function () { + it("should get the index page", async () => { + const { expect, request } = await get_chai(); + const req = request.execute(app).get("/").send(); + const res = await req; + expect(res).to.have.status(200); + expect(res).to.have.property("text"); + expect(res.text).to.include("Click this link"); + }); +}); +``` +In this case, you get a res.text, instead of a res.body. The text is the actual HTML sent back in response to the request, as a string. Checking the string to see if the response was correct can be a little clumsy, as compared with checking the results of an API. Anyway, verify that your tests still pass, by doing ```npm run test```. If you used slightly different wording in your page, you'll have to change the test above. + +## Testing Registration + +Here is a test for registration. You should put it in a file tests/registration_logon.js. +```javascript +const { app } = require("../app"); +const { factory, seed_db } = require("../util/seed_db"); +const faker = require("@faker-js/faker").fakerEN_US; +const get_chai = require("../util/get_chai"); + +const User = require("../models/User"); + +describe("tests for registration and logon", function () { + // after(() => { + // server.close(); + // }); + it("should get the registration page", async () => { + const { expect, request } = await get_chai(); + const req = request.execute(app).get("/session/register").send(); + const res = await req; + expect(res).to.have.status(200); + expect(res).to.have.property("text"); + expect(res.text).to.include("Enter your name"); + const textNoLineEnd = res.text.replaceAll("\n", ""); + const csrfToken = /_csrf\" value=\"(.*?)\"/.exec(textNoLineEnd); + expect(csrfToken).to.not.be.null; + this.csrfToken = csrfToken[1]; + expect(res).to.have.property("headers"); + expect(res.headers).to.have.property("set-cookie"); + const cookies = res.headers["set-cookie"]; + this.csrfCookie = cookies.find((element) => + element.startsWith("csrfToken"), + ); + expect(this.csrfCookie).to.not.be.undefined; + }); + + it("should register the user", async () => { + const { expect, request } = await get_chai(); + this.password = faker.internet.password(); + this.user = await factory.build("user", { password: this.password }); + const dataToPost = { + name: this.user.name, + email: this.user.email, + password: this.password, + password1: this.password, + _csrf: this.csrfToken, + }; + const req = request + .execute(app) + .post("/session/register") + .set("Cookie", this.csrfCookie) + .set("content-type", "application/x-www-form-urlencoded") + .send(dataToPost); + const res = await req; + expect(res).to.have.status(200); + expect(res).to.have.property("text"); + expect(res.text).to.include("Jobs List"); + newUser = await User.findOne({ email: this.user.email }); + expect(newUser).to.not.be.null; + }); +}); + +``` +Ok, there's a lot going on here. The test first gets the registration form. So far so good. Then, the task is to post values for the form so that the user is actually registered. But, to post a form, we have to get past the protection against cross site request forgery that you implemented in the last lesson. To do that, we need the CSRF token, which appears in the form itself, but we have to find it. We can do that using a regular expression. First we take the line ends out of the form, as they mess up regular expression parsing. Then we execute a regular expression to find the token itself. If you don't know regular expressions, they are good to learn, but otherwise just use the one herein provided. When we post the values for the form, we need to include the value for the csrf token. We store it in this.csrfToken, so that we can reuse the value. The other half of the CSRF protection is that we also need to send the cookie. Chai does not keep cookie values between tests. We have to preserve the ones we want, and include them on subsequent requests. Chai doesn't even store the cookies in a very friendly way. We have to parse them out of the response headers, so there is more logic to do that. For each of these steps, we do a Chai assertion (expect) so that we know all is working. + +If one of the expect() assertions fails, the rest of the code in that it() stanza does not run, but execution will continue with the next stanza. + +### A Reminder About Arrow Functions and Non-Arrow Functions + +You will notice that we declare anonymous functions two different ways: +```javascript +describe("tests for registration and logon", function () { +``` +and +```javascript + it("should get the registration page", async () => { +``` +The difference is that arrow functions do not have their own "this"! They inherit the this of the context in which they were defined. So, when we save to the variable this.csrfToken, we do it in the context of the describe(). On that call to describe, we pass ```function ()```, and so the this is associated with that context. As a result this.csrfToken is available on our next it() call within that same describe, so long as that call to it() passes an arrow function. There are, of course, other ways to save the token, such is in a variable with module scope. + +## Posting the Form Values + +Ok, so what do we post, and where do we post it? The post for register is /sessions/register. If we look at the register view, we see what is expected, from the names of the entry fields. These are name, email, password, and password1 (for password confirmation). To get values for these, we can use the user factory created in util/seed_db.js. But (a) we need to save the password, so that we can use it to test logon; (b) we need to save other values for the user, again for logon, and (c) we use factory.build, not factory.create, because we don't want the factory to store values in the database. That's what the actual register operation is supposed to do. + +When we post, we have to set the cookie for CSRF protection. We also have to set the content-type, which would otherwise be JSON. We also have to include the csrfToken in the data that is posted, with the name _csrf. We post the resulting information, and then search the database to verify that the user object was actually created. + +There could be two kinds of it() statements; +```javascript + it("should get the registration page", (done) => { +``` +and +```javascript + it("should register the user", async () => { +``` +In the first (old style) way, we pass a callback, the done() function, and that must be called at the completion of the test. In the second way, we just declare an async function. In our examples, we only use the second way, because we have to call get_chai(), which is asynchronous. + +If the user is actually created, our controller sends a redirect. By default, Chai traverses the redirect automatically, so that the res object coming back should have a status of 200. It should redirect to the index page, and on that page one should see "Click this link to logon". + +### An Aside on Status Codes + +When the controller gets an error from a post, it can render the page again +```javascript + req.flash("error", "That email address is already registered."); + return res.status(400).render("register", { errors: req.flash("error") }); +``` +Be careful to include the status(400). If the status is 200, the request is expected to have succeeded. Check your render statements to make sure that if there is an error condition, the 400 status code is set. I think I provided some code in earlier lessons that did not do that. + +## Testing Logon + +We saved this.user and this.password, so we should be able to log in. We'll skip actually loading the logon form -- you could add that test if you like -- and we'll do the post for logon. When you logon, you are redirected. By default, Chai then follows the redirection, but what it doesn't do is keep the cookies. When you do the .send for the test, the cookies are already gone. This is completely useless for logon. We need the session cookie for subsequent requests. It is pretty poor in another way. If you redirect, the session contains the flash information for user messages, but if the cookies are gone, so are the flash messages. So, a better policy is to disable redirects by doing .redirects(0) on the request. If a redirect occurs, the status is 302, and the req.headers.location is the target for the redirect. (Editorial aside: Chai really ought to save those cookies.) So, here is the logon test, which should be added to the previous describe() section: +```javascript + it("should log the user on", async () => { + const dataToPost = { + email: this.user.email, + password: this.password, + _csrf: this.csrfToken, + }; + const { expect, request } = await get_chai(); + const req = request + .execute(app) + .post("/session/logon") + .set("Cookie", this.csrfCookie) + .set("content-type", "application/x-www-form-urlencoded") + .redirects(0) + .send(dataToPost); + const res = await req; + expect(res).to.have.status(302); + expect(res.headers.location).to.equal("/"); + const cookies = res.headers["set-cookie"]; + this.sessionCookie = cookies.find((element) => + element.startsWith("connect.sid"), + ); + expect(this.sessionCookie).to.not.be.undefined; + }); + + it("should get the index page", async () => { + const { expect, request } = await get_chai(); + const req = request + .execute(app) + .get("/") + .set("Cookie", this.csrfCookie) + .set("Cookie", this.sessionCookie) + .send(); + const res = await req; + expect(res).to.have.status(200); + expect(res).to.have.property("text"); + expect(res.text).to.include(this.user.name); + }); +``` +There are two parts to the test. The first does the logon. You get a redirect ... but it will redirect to the same place whether the logon succeeds or fails. And you will have a session cookie even before you log in. So how do you know whether the logon succeeded? The only way is to get the index page again. If the logon is successful, it will show the user's name, but if not, it will show the error message. To do this, we have to include the session cookie in the request, as we do above. + +**Now: Some code for you to write.** Create a test for logoff. Logoff won't work unless there has been a logon, and unless you send the _csrf value and set cookies for both the csrfToken and the sessionCookie. The latter code is: +```javascript +.set("Cookie", this.csrfToken + ";" + this.sessionCookie) +``` +You need to post data, as before, but the only field in the data is ```_csrf```. In this case, you let Chai follow the redirect, that is, do not do ```.redirects(0)```. You should get back a page that includes "link to logon". + +## Testing Job CRUD Operations + +Create a new file, tests/crud_operations.js. You will need a couple of extra require() statements, as follows: +```javascript +const Job = require("../models/Job") +const { seed_db, testUserPassword } = require("../util/seed_db"); +``` +The flow for testing CRUD operations is as follows. + +1. Seed the database! You have a utility routine for that in util/seed_db.js +2. Logon! You will have to get the logon page to get the CSRF token and cookie. The seed_db.js module has a function to seed the database with a user entry, and it also exports the user's password, so you can use those. You'll need to save the session cookie. Steps 1 and 2 are not tests, but you need an async before() call, inside your describe(), that does these things. Here is the before() that completes steps 1 and 2: + ```javascript + before(async () => { + const { expect, request } = await get_chai(); + this.test_user = await seed_db(); + let req = request.execute(app).get("/session/logon").send(); + let res = await req; + const textNoLineEnd = res.text.replaceAll("\n", ""); + this.csrfToken = /_csrf\" value=\"(.*?)\"/.exec(textNoLineEnd)[1]; + let cookies = res.headers["set-cookie"]; + this.csrfCookie = cookies.find((element) => + element.startsWith("csrfToken"), + ); + const dataToPost = { + email: this.test_user.email, + password: testUserPassword, + _csrf: this.csrfToken, + }; + req = request + .execute(app) + .post("/session/logon") + .set("Cookie", this.csrfCookie) + .set("content-type", "application/x-www-form-urlencoded") + .redirects(0) + .send(dataToPost); + res = await req; + cookies = res.headers["set-cookie"]; + this.sessionCookie = cookies.find((element) => + element.startsWith("connect.sid"), + ); + expect(this.csrfToken).to.not.be.undefined; + expect(this.sessionCookie).to.not.be.undefined; + expect(this.csrfCookie).to.not.be.undefined; + }); + ``` +3. Get the job list! You have to include the session cookie with your get request. The seed operation stores 20 entries. Your test should verify that a status 200 is returned, and that exactly 20 entries are returned. That's a little complicated for an html page, but in this case, you can just check how many times "" appears on the page. Here's how you might do that part: + ```javascript + const pageParts = res.text.split("") + expect(pageParts).to.equal(21) + ``` + As you can see, scanning the page to see if the result is correct is kind of messy. +4. Add a job entry! This is a post for the job form. You will have to include _csrf in the post, and you will need to set the CSRF and session cookies. You could use the factory to create values for the job, via a factory.build('job'). The best way to test for success is to see that the database now has 21 entries, as follows: + ```javascript + const jobs = await Job.find({createdBy: this.test_user._id}) + expect(jobs.length).to.equal(21) + ``` + +## Puppeteer + +In actual practice, chai-http is mostly used for testing APIs. To test a user interface, whether it be server side rendered or full stack, one would use an actual browser testing engine such as puppeteer. Create a file, tests/puppeteer.js, with the following contents: +```javascript +const puppeteer = require("puppeteer"); +require("../app"); +const { seed_db, testUserPassword } = require("../util/seed_db"); +const Job = require("../models/Job"); + +let testUser = null; + +let page = null; +let browser = null; +// Launch the browser and open a new blank page +describe("jobs-ejs puppeteer test", function () { + before(async function () { + this.timeout(10000); + //await sleeper(5000) + browser = await puppeteer.launch(); + page = await browser.newPage(); + await page.goto("http://localhost:3000"); + }); + after(async function () { + this.timeout(5000); + await browser.close(); + }); + describe("got to site", function () { + it("should have completed a connection", async function () {}); + }); + describe("index page test", function () { + this.timeout(10000); + it("finds the index page logon link", async () => { + this.logonLink = await page.waitForSelector( + "a ::-p-text(Click this link to logon)", + ); + }); + it("gets to the logon page", async () => { + await this.logonLink.click(); + await page.waitForNavigation(); + const email = await page.waitForSelector('input[name="email"]'); + }); + }); + describe("logon page test", function () { + this.timeout(20000); + it("resolves all the fields", async () => { + this.email = await page.waitForSelector('input[name="email"]'); + this.password = await page.waitForSelector('input[name="password"]'); + this.submit = await page.waitForSelector("button ::-p-text(Logon)"); + }); + it("sends the logon", async () => { + testUser = await seed_db(); + await this.email.type(testUser.email); + await this.password.type(testUserPassword); + await this.submit.click(); + await page.waitForNavigation(); + await page.waitForSelector(`p ::-p-text(${testUser.name} is logged on.)`); + await page.waitForSelector("a ::-p-text(change the secret)"); + await page.waitForSelector('a[href="/secretWord"]'); + const copyr = await page.waitForSelector("p ::-p-text(copyright)"); + const copyrText = await copyr.evaluate((el) => el.textContent); + console.log("copyright text: ", copyrText); + }); + }); +}); +``` +In each of the describe() stanzas, as well as in before() and after(), we call this.timeout() to set a reasonable number of milliseconds after which the operation should be abandoned. The puppeteer.launch() call actually launches the browser, which by default is a version of Chrome. The browser.newPage() call opens up a browser page. The page.goto() call opens the root page of the application being tested. Then, we have the following calls: + +- page.waitForSelector(): Waits for DOM entry matching the selector to appear on the page. +- page.waitForNavigation(): Waits for the next page to display. +- entry.type(): Types a value into an entry field. +- entry.click(): Clicks on a button or other control. + +You can see that these allow you to traverse the application, and there are other operations as well, which you can find in the online documentation for puppeteer [here](https://pptr.dev/). The waitForSelector() function takes one argument, which is the selector. These come in two flavors: + +- CSS style selectors: ```await page.waitForSelector('a[href="/secretWord"]');```. This finds a link to the /secretWord page. +- P selectors, a puppeteer add on: ```await page.waitForSelector("p ::-p-text(copyright)");``` This one finds a paragraph element with the text "copyright" in it. + +The test uses these to click on the link for the logon page, to fill out the page, and to verify that the right page came back and the logon completed. The seed_db utility is used to create a user that is used for the logon, and to create some job entries belonging to that user. Run ```npm run test``` to verify that all still works. + +Now, if you like, you can watch the process. If you change the puppeteer.launch() statement to read +``` +puppeteer.launch({headless: false, slowMo: 100}) +``` +and then rerun the test, you'll see the test in progress. However, an aside for Windows users: If you are in the habit of doing your development in the Windows Linux Subsystem, the headless:false may or may not work. + +## More Code to Write + +The test you are to add is to verify that the job operations work correctly. You will need to add the expect function from Chai to the test. You can do: +``` +const { expect } = await import('chai') +``` +but as this is an async call, you can only do this in the it() sections where you need it. +1. Add a new describe("puppeteer job operations" ...) stanza for this series of tests. +2. (test1) Make the test do a click on the link for the jobs list. Verify that the job listings page comes up, and that there are 20 entries in that list. + A hint here: page.content() can be used to get the entire HTML page, and you can use the split() function on that to find the ``````entries. +3. (test2) Have the test click on the "Add A Job" button and to wait for the form to come up. Verify that it is the expected form, and resolve the company and position fields and add button. +4. (test3) Type some values into the company and position fields. Then click on the add button. Wait for the jobs list to come back up, and verify that the message says that the job listing has been added. Check the database to see that the latest jobs entry has the data entered. You also use Job.find() as in the previous exercise.) + +## Submit Your Code + +As usual. \ No newline at end of file diff --git a/lessons/ctd-node-assignment-2.md b/lessons/ctd-node-assignment-2.md new file mode 100644 index 0000000000..10751ad4b0 --- /dev/null +++ b/lessons/ctd-node-assignment-2.md @@ -0,0 +1,184 @@ +For week 1, you created files in the `node-express-course/01-node-tutorial/answers` directory. For week 2, you’ll continue to do your work in that same directory. However, before you do your work for this week, you must switch to a new git branch. While the week1 branch is active, use the command + +``` +git checkout -b week2 +``` + +As you will be working on the `node-express-course` repository for some weeks, this is the way you separate your assignments for each week. The key topics in this section are: + +* how to use `npm` and the `package.json` file to manage a Node project and its dependencies +* async patterns +* event emitters and handlers +* streams + +The instructor does provide examples if you need them. Just like the previous lesson, you will work in the `answers` directory within the `01-node-tutorial` directory. Follow these steps: + +1. Within your answers directory, create a file called `.gitignore`. It should have the following lines: +``` +/node_modules +.DS_Store +``` +You do not want to store the contents of `node_modules` in Github, because they are already present on the web as public files, accessible by npm. As well, the `node_modules` folder can get very large, which would slow down our git operations if we include it. The `.DS_Store` file is sometimes created by the Mac operating system, and you don’t need that one in Github either. +2. Within your `answers` directory, run the command `npm init`. You can accept all the defaults, except you can enter your name when it prompts you for author. This creates a `package.json` file. +3. Enter the following command: +``` + npm install nodemon --save-dev +``` +As the instructor has described, `npm` gives you access to a large library of reusable code, available at [npmjs.com](https://www.npmjs.com/). You have just installed one package, but you have also installed all its dependencies, and they are all stored in the `./node_modules` directory. You can see what you have installed by looking at package-lock.json (which is automatically generated and updated whenever you install, update, or remove packages). You will never need to manually modify the `package-lock.json` file, though you _can_ make changes to the `package.json` file. The package you’ve just installed `nodemon`, is very useful for development, but you wouldn’t want to deploy it to the cloud, as it is not useful in production, so it is installed as a “dev dependency”. +4. Edit the `package.json` file. This file manages your project, enabling others to contribute and also providing a means to deploy the project to the cloud. You can read a description of package.json [here](https://nodesource.com/blog/the-basics-of-package-json-in-node-js-and-npm/). There is one other entry that is often useful, called engines. This can be used to specify which version of node your package requires. But for now, you are just going to set up the scripts stanza. Edit it to look like the following: +``` + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "nodemon prompter.js", + "start": "node prompter.js" + }, +``` +The scripts give npm commands you can run as you develop. In this case, the command, `npm run dev` will run the code in the `prompter.js` file, using the `nodemon` library. `nodemon` will basically just run the file with `node`, and if you make any changes to the program, `nodemon` will automatically restart node for you so that it’s running the updated code. This is useful to test continuously as you are changing your program. The command `npm run start` or just `npm start` runs the program under `node`, which is how it would run in production. +5. From within your terminal, while you are in the `answers` directory, run the command `npm run dev`. The prompter program starts, and you can access the page from http://localhost:3000 in your browser. Now edit `prompter.js` and add a comment. Nodemon restarts the program, because `prompter.js` has changed. As before, you use Ctrl+c to end the program. Nodemon is only important for programs that keep running until you press Ctrl+c. It is not needed for programs that end by themselves. + +Now for some code. The instructor showed several patterns for JavaScript asynchronous programming. An asynchronous Javascript function returns a [Promise](https://javascript.info/promise-basics), or in some cases a “thenable” which acts like a Promise. You need to resolve the Promise in order to get the actual return value. To resolve a Promise inside an `async function`, you use the keyword, `await`. This should be used inside a `try/catch` block so that you can handle any errors, as follows: + +```javascript +const myFunc = async () => { + ... + return result +} + +const myFunc2 = async () => { + try { + result = await myFunc() + ... + } catch(err) { + console.log("An error occurred: ", err) + } +} +``` + +Sometimes you need to call an async function from within another function that +is not async. In this case, you can’t use `await` – it will give a +syntax error. So you can either use `.then` or you can wrap the +function call as follows: + +```javascript +const myFunc3 = () => { // not async, and in some contexts we better not make it async + myFunc() + .then((result) => { + console.log("got the result.") + ... + }) + .catch((error)=> { + console.log("An error occurred: ", error) + }) +} + +const myFunc4 = () => { // the other way to do it, via a wrapper: + const myFunc5 = async () => { + try { + result = await myFunc() { + console.log("got the result.") + ... + } + } catch(error) => { + console.log("An error occurred: ", error) + } + } + myFunc5() // here's where we call the wrapper, but do NOT do this: + // result = myFunc5() This won't work because myFunc5 is asynchronous! + // All you'd get back is a Promise, not the result. +} +``` + +There’s one more trick with the `.then`. Suppose you need to make a string of calls to async functions. You can chain the `.then` statements as follows: + +```javascript +const myFunc6 = () => { + myFunc() // an async function, so it returns a promise + .then((result) => { + console.log("got the first result"); + return myFunc(); // here we call it again, we return the promise myFunc returns + }) + .then((result) => { + console.log("got the second result"); + }) + .catch((err) => { + console.log("An error occurred: ", err); + }); +}; +``` + +So, you can chain a collection of async calls with `.then` +statements, followed by one `.catch` at the end. + +Ok, that’s the summary. Be sure that you understand this. We will do a lot of asynchronous programming in Express. + +--- + +Now for the programs to write for this assignment: + +1. Create a program named `writeWithPromisesAwait.js` inside the `01-node-tutorial/answers` folder. We are going to use the fs.promises package. `fs` is the built-in “File system” set of functions in [Node](https://nodejs.org/api/fs.html#promises-api). By adding `.promises` we’re going to access the versions of those built-in functions that return a Promise as their result. You’d start with the following code: +``` +const { writeFile, readFile } = require("fs").promises; +``` +Then create an `async function` called `writer` that takes 0 arguments, and that writes three lines to a file named temp.txt, by calling the `writeFile` function with `await`. The Promise version of `writeFile` takes the same arguments as the one you used in last week’s exercise `10-fs-sync` but will return a Promise instead of a result directly. +**Put the await statements inside a `try/catch` block!** +Create another async function called `reader` that reads the file with `await readFile` and logs the return value to the screen. +Now we want to call the two functions in order, first the writer, and the reader. But, be careful! These are asynchronous functions, so if you just call them, you don’t know what order they’ll occur in. And you can’t use await in your mainline code. So, you write a third async function called `readWrite`. In that function, you call await reader and await writer. Finally, write a line at the bottom of the file that calls the `readWrite` function. Test your code. The temp.txt file that your code is creating should not be sent to Github, so you should add this filename as +another line to your `.gitignore.` +2. Write another program called `writeWithPromisesThen.js` also in the `01-node-tutorial/answers` folder. Again you write to temp.txt. You start it the same way, but this time, you use the `.then` style of asynchronous programming. You don’t need to create any functions. Instead, you just use cascading .then statements in your mainline, like this: +```javascript + writeFile(...) // write line 1 + .then(() => { + return writeFile(...) // write line 2. + // Return the promise so you can chain the .then statements + }) + .then // write the third line, and follow that with two more .then blocks, + // one to call readFile to read it back out, and one to log the data to the screen. + ... + .catch((error) => { + console.log("An error occurred: ", error) + }) +``` +Test your code by running `node writeWithPromisesThen.js`. You may +want to sprinkle console.log statements in your code so that you understand +the order of execution. +3. We want to understand event emitters. First, modify `prompter.js`, to add the following lines above the listen statement: +```javascript +server.on("request", (req) => { + console.log("event received: ", req.method, req.url); +}); +``` +Then test this (`npm run dev`) and try with your browser to see the events the server is emitting. +4. Write a program named `customEmitter.js` in the `01-node-tutorial/answers` folder. In it, create one or several emitters. Then use the emitter `.on` function to handle the events you will emit, logging the parameters to the screen. Then use the emitter `.emit` function to emit several events, with one or several parameters, and make sure that the events are logged by your event handlers. This is your chance to be creative! You could have an event handler that emits a different event to be picked up by a different handler, for example. Here’s a couple tricks to try. You can trigger events with a timer, as follows: +```javascript +const EventEmitter = require("events"); +const emitter = new EventEmitter(); +setInterval(() => { + emitter.emit("timer", "hi there"); +}, 2000); +emitter.on("timer", (msg) => console.log(msg)); +``` +Or, you could make an async function that waits on an event: +```javascript +const EventEmitter = require("events"); +const emitter = new EventEmitter(); +const waitForEvent = () => { + return new Promise((resolve) => { + emitter.on("happens", (msg) => resolve(msg)); + }); +}; +const doWait = async () => { + const msg = await waitForEvent(); + console.log("We got an event! Here it is: ", msg); +}; +doWait(); +emitter.emit("happens", "Hello World!"); +``` +(Don’t worry if that last one looks a bit complicated. That’s expected as we +haven’t talked about creating promises yet.) +5. Change back to the `01-node-tutorial` directory and run `15-create-big-file.js`. This creates a big file in the content directory. You’ll note that the instructor has a `.gitignore` that includes the file name that’s created by that code so that it isn’t stored in Github. +6. Now, change back to the `answers` directory, and write a program called `16-streams.js`. It should create a read stream for the big file (`../content/big.txt`) with encoding of `"utf8"` and a `highWaterMark` of `200`. The `highWaterMark` is the maximum amount of bytes that node will read with each chunk of the stream. The program should initialize a counter to 0\. Then it should handle the `“data”` event for the stream by incrementing the counter and logging the event result to the screen. Then it should handle the `“end”` event by reporting the number of chunks received. Finally, it should handle the stream `“error”` event by logging the error to the console. Test the program for several values of highWaterMark. You can look at `01-node-tutorial/16-streams.js` file to help you as needed. +7. Have a look at `17-http-stream.js`. You don’t need to write a program, but observe how the chunks that are read from the stream are piped to the `res` object that is returned from the http request. Try to understand this program. Usually, if you are sending back voluminous data in response to an HTTP request, you want to break it up into chunks. + +That’s all for this week, great job! Note, that while the `01-node-tutorial` folder has files for `13-event-emitter.js` and `14-request-event.js`, we’ve substituted those with the event emitter work in this week’s assignment and the `prompter.js` work in the previous assignment. You can still feel free to follow along with the video and complete those files if you’d like. + +When you have completed your programming assignment, do a `git add` for all your changes to the branch, commit the changes, and push the changes to your repository. Then create a pull request. A link to the pull request is to be included in your homework submisson. \ No newline at end of file diff --git a/lessons/ctd-node-assignment-3.md b/lessons/ctd-node-assignment-3.md new file mode 100644 index 0000000000..8552bfee7d --- /dev/null +++ b/lessons/ctd-node-assignment-3.md @@ -0,0 +1,56 @@ +The basic elements of an Express.js program are as follows: + +* The `require` statement to import the `express` module +* Creation of the app as returned from calling `express()` +* `app.use` statements for the middleware. You’ll eventually use many kinds of middleware, but for now the only middleware we are using is `express.static()`. +* `app.get` and `app.post` statements for the routes you will handle. Eventually these will be refactored into router modules, but for now you can put them inline. +* An `app.all` statement after these to handle page not found conditions. +* An `app.listen` statement to tell the server to listen for requests on a particular port. + +You continue working in the node-express-course repository, but for this week, you switch to the 02-express-tutorial directory. This week introduces the Express npm package, which makes web development much quicker than using Node alone. There is no need to use an answers folder. You just put your work in the 02-express-tutorial folder. Complete the following steps: + +1. While the `week2` branch is active, create a `week3` branch, for this week’s work (`git checkout -b week3`). +2. While you are in the `02-express-tutorial` folder, run `npm install`. The instructor has provided a `package.json` and a `.gitignore`. The `package.json` already has the express package defined in it, so running the command `npm install` will do the installation of that package, as needed for this week’s work. +3. Create a folder named `public` within `02-express-tutorial`. Create an HTML file within it called `index.html`. It’s not critical what you put in the HTML file – just something simple. +4. Edit `app.js` to add all the elements of an Express application as listed above, in the right order. You won’t have any `app.get` or `app.post` statements yet. You should have the statement `app.use(express.static("./public"))` so that your HTML file will load. Use port 3000 in the listen statement. +5. Start the server, with `npm start`. Then use your browser to load http://localhost:3000. You should see the HTML page you created. +6. Try the URL http://localhost:3000/not-there. You should see that your `app.all` for page not found returns a 404 error. +7. For the next part, you will implement APIs that return JSON. Because you are using the browser to display the JSON, you may want to add a JSON formatter plugin into your browser ([here’s one for Chrome](https://chrome.google.com/webstore/detail/jsonvue/chklaanhfefbnpoihckbnefhakgolnmc), for example), so that it’s easier to view. Add an `app.get` statement to `app.js`. It should be _after_ the Express static middleware, but _before_ the “not found” handler. It should be for the URL `/api/v1/test`. It should return JSON using the following code: + +```javascript +res.json({ message: "It worked!" }); +``` + +Try that URL from your browser, and verify that it works. + +1. Next, we want to return some data. We haven’t learned how to access a database from Express yet, so the instructor has provided data to use. It is in `data.js`, so have a look at that file. Then add the following require statement to the top of the program: + +```javascript +const { products } = require("./data"); +``` + +The value of the products variable is an array of objects from +`data.js`, which are various items of furniture. We now want to +return this array. So add an `app.get` statement for the url +`/api/v1/products`. Write some code to return JSON for the products +array. Test the url with your browser. + +1. Next, you need to provide a way to retrieve a particular product by ID. This is done by having an `app.get` statement for the url `/api/v1/products/:productID`. The colon in this url means that `:productID` is a _parameter_. So, when your server receives the GET request for a URL like `/api/v1/products/7`, `req.params` will have the hash `{ productID: 7 }`. Try this out by creating the `app.get` statement and doing a `res.json(req.params)` to return the path parameter in the HTTP response itself. +2. Of course, the API should actually return, in JSON form, the product that has an ID of 7\. So you need to find that product in the array. For that, you use the `.find` [function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Array/find) of the array: + +```javascript +const idToFind = parseInt(req.params.productID); +const product = products.find((p) => p.id === idToFind); +``` + +The parseInt is needed because query parameters are always passed as strings, so you have to convert this to an integer. Change the app.get statement so that it returns JSON for the product. Test it out. + +1. The user may request a product that is not there, for example with a URL like `/api/v1/products/5000` or `/api/v1/products/nottthere`. So in that case, you should return a 404 status code and the JSON `{ message: "That product was not found."}`. Add this logic to the `app.get` statement, and test that it works. +2. The user may also want to do a simple search, instead of getting all the products. In this case, the url would contain a query string, like: `/api/v1/query?search=al&limit=5`. +What this means, in this case, is that the user wants to see all products where the name starts with “al”, but the user wants to see no more than 5 products. When the `app.get` for `/api/v1/query` path is called, `req.query` is a hash that may contain values for “search” or “limit” or both or neither, depending on what the user puts in the query string. Again, there are array methods you can use to find that list. They are `Array.filter()` and `Array.slice()`. Add a new `app.get` statement for `/api/v1/query`, and include logic to handle these query strings. Then test it out. +3. Add some more logic: you choose! For example, the user might want to send a regular expression instead of search for starting letters. Or the user may only want products that cost less than 20.00. +4. Optional additional item: Add a button to your `index.html`. Add JavaScript, either within a `