diff --git a/week-2/week-2-async-js/solutions/easy/1-counter.js b/week-2/week-2-async-js/solutions/easy/1-counter.js index da2806780..80cade2d8 100644 --- a/week-2/week-2-async-js/solutions/easy/1-counter.js +++ b/week-2/week-2-async-js/solutions/easy/1-counter.js @@ -1,9 +1,13 @@ let counter = 0; +// const updateCounter = () => { +// counter++; +// console.log(counter); +// }; -const updateCounter = () => { +function updateCounter() { counter++; console.log(counter); -}; +} setInterval(updateCounter, 1000); diff --git a/week-2/week-2-async-js/solutions/easy/2-counter.js b/week-2/week-2-async-js/solutions/easy/2-counter.js index 95896a264..38e3a69b7 100644 --- a/week-2/week-2-async-js/solutions/easy/2-counter.js +++ b/week-2/week-2-async-js/solutions/easy/2-counter.js @@ -1,10 +1,16 @@ let counter = 0; -const updateCounter = () => { - counter++; - console.log(counter); +// const updateCounter = () => { +// counter++; +// console.log(counter); + +// setTimeout(updateCounter, 1000); +// }; +function updateCounter() { + counter++; + console.log(counter); setTimeout(updateCounter, 1000); -}; +} updateCounter(); diff --git a/week-2/week-2-async-js/solutions/easy/example.txt b/week-2/week-2-async-js/solutions/easy/example.txt index a542373a3..5dd01c177 100644 --- a/week-2/week-2-async-js/solutions/easy/example.txt +++ b/week-2/week-2-async-js/solutions/easy/example.txt @@ -1 +1 @@ -Hello, world!!! \ No newline at end of file +Hello, world! \ No newline at end of file diff --git a/week-2/week-2-async-js/solutions/hard/1-promisify-setTimeout.js b/week-2/week-2-async-js/solutions/hard/1-promisify-setTimeout.js index 1362af622..94f15cc7a 100644 --- a/week-2/week-2-async-js/solutions/hard/1-promisify-setTimeout.js +++ b/week-2/week-2-async-js/solutions/hard/1-promisify-setTimeout.js @@ -7,4 +7,14 @@ function wait(n) { return p; } -module.exports = wait; \ No newline at end of file +module.exports = wait; + +function randomAfter3S(resolve, n) { + setTimeout(resolve, n*1000); +} + +function callbackAfter3S() { + console.log("promise succeeded after 3 seconds!") +} +p = new Promise((resolve) => randomAfter3S(resolve, 3)); +p.then(callbackAfter3S); \ No newline at end of file diff --git a/week-2/week-2-async-js/solutions/hard/2-sleep-completely.js b/week-2/week-2-async-js/solutions/hard/2-sleep-completely.js index 42e80a1a4..3cb056fea 100644 --- a/week-2/week-2-async-js/solutions/hard/2-sleep-completely.js +++ b/week-2/week-2-async-js/solutions/hard/2-sleep-completely.js @@ -6,4 +6,16 @@ function sleep(milliseconds) { }); } -module.exports = sleep; \ No newline at end of file +module.exports = sleep; + +function randomAfterSleep(resolve, n) { + let startTime = new Date().getTime(); + while (new Date().getTime() < startTime + n*1000); + resolve(); +} + +function callbackAfterSleep() { + console.log("promise succeeded after sleeping for 3 seconds!") +} +p = new Promise((resolve) => randomAfterSleep(resolve, 3)); +p.then(callbackAfterSleep); \ No newline at end of file diff --git a/week-2/week-2-async-js/solutions/hard/3-promise-all.js b/week-2/week-2-async-js/solutions/hard/3-promise-all.js index 5ca428d5a..3ce15a9a6 100644 --- a/week-2/week-2-async-js/solutions/hard/3-promise-all.js +++ b/week-2/week-2-async-js/solutions/hard/3-promise-all.js @@ -25,5 +25,53 @@ function wait1(t) { return totalTime; } - module.exports = calculateTime; - \ No newline at end of file +module.exports = calculateTime; + + +// function randomAfter1S(resolve, n) { +// setTimeout(resolve, n*1000); +// } + +function callbackAfter1S() { + console.log("promise succeeded after 1 seconds!") +} +let p1 = new Promise((resolve) => randomAfterNS(resolve, 1)); +p1.then(callbackAfter1S); + + + +// function randomAfter2S(resolve, n) { +// setTimeout(resolve, n*1000); +// } + +function callbackAfter2S() { + console.log("promise succeeded after 2 seconds!") +} +let p2 = new Promise((resolve) => randomAfterNS(resolve, 2)); +p2.then(callbackAfter2S); + + + +// function randomAfter3S(resolve, n) { +// setTimeout(resolve, n*1000); +// } + +function callbackAfter3S() { + console.log("promise succeeded after 3 seconds!") +} +let p3 = new Promise((resolve) => randomAfterNS(resolve, 3)); +p3.then(callbackAfter3S); + +console.log("hello") + +function randomAfterNS(resolve, n) { + setTimeout(resolve, n*1000); +} + +const startTime = Date.now(); + +function callback() { + console.log("all promises succeeded after " + (Date.now() - startTime)/1000 + " seconds!") +} + +Promise.all([p1, p2, p3]).then(callback); \ No newline at end of file diff --git a/week-2/week-2-async-js/solutions/hard/4-promise-chain.js b/week-2/week-2-async-js/solutions/hard/4-promise-chain.js index f3608d78d..92756d918 100644 --- a/week-2/week-2-async-js/solutions/hard/4-promise-chain.js +++ b/week-2/week-2-async-js/solutions/hard/4-promise-chain.js @@ -32,4 +32,93 @@ function call(t1, t2, t3) { }); } -module.exports = calculateTime; \ No newline at end of file +module.exports = calculateTime; + +// function randomAfter1S(resolve, n) { +// setTimeout(resolve, n*1000); +// } + +function callbackAfter1S() { + console.log("promise succeeded after 1 seconds!") +} +let p1 = new Promise((resolve) => randomAfterNS(resolve, 1)); +// p1.then(callbackAfter1S); + + + +// function randomAfter2S(resolve, n) { +// setTimeout(resolve, n*1000); +// } + +function callbackAfter2S() { + console.log("promise succeeded after 2 seconds!") +} +// let p2 = new Promise((resolve) => randomAfterNS(resolve, 2)); +// p2.then(callbackAfter2S); + + + +// function randomAfter3S(resolve, n) { +// setTimeout(resolve, n*1000); +// } + +function callbackAfter3S() { + console.log("promise succeeded after 3 seconds!") +} +// let p3 = new Promise((resolve) => randomAfterNS(resolve, 3)); +// p3.then(callbackAfter3S); + +console.log("hello") + +function randomAfterNS(resolve, n) { + setTimeout(resolve, n*1000); +} + +const startTime = Date.now(); + +function callback() { + console.log("all promises succeeded after " + (Date.now() - startTime)/1000 + " seconds!") +} + +p1 + .then(() => { + callbackAfter1S(); + return new Promise((resolve) => randomAfterNS(resolve, 2)); + }) + .then(() => { + callbackAfter2S(); + return new Promise((resolve) => randomAfterNS(resolve, 3)); + }) + .then(() => { + callbackAfter3S(); + callback(); + }); + + +p1 = new Promise((resolve) => randomAfterNS(resolve, 1)); +p1.then(() => { + callbackAfter1S(); + p2 = new Promise((resolve) => randomAfterNS(resolve, 2)); + p2.then(() => { + callbackAfter2S(); + p3 = new Promise((resolve) => randomAfterNS(resolve, 3)); + p3.then(() => { + callbackAfter3S(); + callback(); + }) + }) + }) + + async function solve() { + await new Promise((resolve) => randomAfterNS(resolve, 1)); + callbackAfter1S(); + await new Promise((resolve) => randomAfterNS(resolve, 2)); + callbackAfter2S(); + await new Promise((resolve) => randomAfterNS(resolve, 3)); + callbackAfter3S(); + callback(); + } + + solve() + + console.log("whatsup") \ No newline at end of file diff --git "a/week-3/easy/The-Pok\303\251mon/index.html" "b/week-3/easy/The-Pok\303\251mon/index.html" new file mode 100644 index 000000000..a8a7ab2f4 --- /dev/null +++ "b/week-3/easy/The-Pok\303\251mon/index.html" @@ -0,0 +1,16 @@ + + + + + index + + + + Hello world + +
+
+ + + + \ No newline at end of file diff --git "a/week-3/easy/The-Pok\303\251mon/script.js" "b/week-3/easy/The-Pok\303\251mon/script.js" new file mode 100644 index 000000000..dc19e10c4 --- /dev/null +++ "b/week-3/easy/The-Pok\303\251mon/script.js" @@ -0,0 +1,50 @@ +let score = 0; + +function startPokemon() { + document.querySelector("#input").innerHTML = "" + document.querySelector("#pokemon-list").innerHTML = "" + document.querySelector("#input").appendChild(submitPokemonId()) +} + +function submitPokemonId() { + const div = document.createElement("div"); + const form = document.createElement("form"); + const label = document.createElement("label"); + const input = document.createElement("input"); + const button = document.createElement("button"); + label.setAttribute("for", "pokemonId") + label.innerHTML = "Enter pokemon id" + input.setAttribute("id", "pokemonId") + input.setAttribute("placeholder", "Enter pokemon id") + button.innerHTML = "Submit" + button.type = "button"; + button.addEventListener("click", () => pokemonComponent(input.value)); + form.appendChild(label) + form.appendChild(input) + form.appendChild(button) + div.appendChild(form) + return div +} + +async function pokemonComponent(id) { + console.log(id) + document.querySelector("#pokemon-list").innerHTML = "" + try { + const response = await fetch("https://pokeapi.co/api/v2/pokemon-form/" + id + "/"); + const pokemonData = await response.json(); // <-- JSON stored here + const h1 = document.createElement("h1"); + const imgFront = document.createElement("img"); + const imgBack = document.createElement("img"); + h1.innerHTML = pokemonData.pokemon.name; + imgFront.setAttribute("src", pokemonData.sprites.front_default) + imgBack.setAttribute("src", pokemonData.sprites.back_default) + document.querySelector("#pokemon-list").appendChild(h1) + document.querySelector("#pokemon-list").appendChild(imgFront) + document.querySelector("#pokemon-list").appendChild(imgBack) + } catch (err) { + console.error("Error fetching Pokémon data:", err); + } +} + +// Attach event listener in JavaScript (module-safe) +document.getElementById("start-btn").addEventListener("click", startPokemon); \ No newline at end of file diff --git a/week-3/easy/bg-color-changer/index.html b/week-3/easy/bg-color-changer/index.html new file mode 100644 index 000000000..48a7cabae --- /dev/null +++ b/week-3/easy/bg-color-changer/index.html @@ -0,0 +1,21 @@ + + + + + + + index + + + + + Hello world + + + + +
hellooooo
+ + + + \ No newline at end of file diff --git a/week-3/easy/bg-color-changer/script.js b/week-3/easy/bg-color-changer/script.js new file mode 100644 index 000000000..642b06c3c --- /dev/null +++ b/week-3/easy/bg-color-changer/script.js @@ -0,0 +1,12 @@ +function changeBackground(colour) { + document.querySelector("body").style.backgroundColor = colour + render(); +} + +// react +function render() { + +} + +// THIS makes the default appear immediately +render(); \ No newline at end of file diff --git a/week-3/easy/quiz-app/index.html b/week-3/easy/quiz-app/index.html new file mode 100644 index 000000000..7382f4a96 --- /dev/null +++ b/week-3/easy/quiz-app/index.html @@ -0,0 +1,35 @@ + + + + + index + + + + Hello world + + + +
+ + + + + \ No newline at end of file diff --git a/week-3/easy/quiz-app/script.js b/week-3/easy/quiz-app/script.js new file mode 100644 index 000000000..8c4ea93d6 --- /dev/null +++ b/week-3/easy/quiz-app/script.js @@ -0,0 +1,99 @@ +import { quizData } from "./data.js"; + +let score = 0; + +function startQuiz() { + score = 0; + render(0); +} + +function submitAnswer(i) { + calculateScore(document.getElementById(quizData[i].correct).checked) + i = i + 1; + if (i == quizData.length) { + document.querySelector("#quiz-list").innerHTML = "Your final score is " + score + " out of " + quizData.length; + } + else + render(i) +} + +function calculateScore(i) { + score = score + i; + console.log("score") + console.log(score) +} + +function quizComponent(i) { + const div = document.createElement("div"); + const h1 = document.createElement("h1"); + const form = document.createElement("form"); + const label1 = document.createElement("label"); + const label2 = document.createElement("label"); + const label3 = document.createElement("label"); + const label4 = document.createElement("label"); + const input1 = document.createElement("input"); + const input2 = document.createElement("input"); + const input3 = document.createElement("input"); + const input4 = document.createElement("input"); + const button = document.createElement("button"); + h1.innerHTML = quizData[i].question; + button.innerHTML = "Submit" + button.type = "button"; + button.addEventListener("click", () => submitAnswer(i)); + + input1.setAttribute("type", "radio") + input1.setAttribute("name", "selection") + input1.setAttribute("value", "a") + input1.setAttribute("id", "a"); + + input2.setAttribute("type", "radio") + input2.setAttribute("name", "selection") + input2.setAttribute("value", "b") + input2.setAttribute("id", "b"); + + input3.setAttribute("type", "radio") + input3.setAttribute("name", "selection") + input3.setAttribute("value", "c") + input3.setAttribute("id", "c"); + + input4.setAttribute("type", "radio") + input4.setAttribute("name", "selection") + input4.setAttribute("value", "d") + input4.setAttribute("id", "d"); + + label1.setAttribute("for", "a") + label2.setAttribute("for", "b") + label3.setAttribute("for", "c") + label4.setAttribute("for", "d") + + div.appendChild(h1) + label1.innerHTML = quizData[i].a; + label2.innerHTML = quizData[i].b; + label3.innerHTML = quizData[i].c; + label4.innerHTML = quizData[i].d; + label1.appendChild(input1) + label2.appendChild(input2) + label3.appendChild(input3) + label4.appendChild(input4) + form.appendChild(label1) + form.appendChild(document.createElement("br")) + form.appendChild(label2) + form.appendChild(document.createElement("br")) + form.appendChild(label3) + form.appendChild(document.createElement("br")) + form.appendChild(label4) + form.appendChild(document.createElement("br")) + form.appendChild(button) + div.appendChild(form) + return div + +} + +// react +function render(i) { + document.querySelector("#quiz-list").innerHTML = "" + document.querySelector("#quiz-list").appendChild(quizComponent(i)) +} + +// Attach event listener in JavaScript (module-safe) +document.getElementById("start-btn").addEventListener("click", startQuiz); \ No newline at end of file diff --git a/week-5/README.md b/week-5/README.md index b447375dc..4dec5e74d 100644 --- a/week-5/README.md +++ b/week-5/README.md @@ -20,14 +20,16 @@ cp .env.example .env 3. run the server. ``` -npm run dev +C:\Users\tavis\OneDrive\Desktop\cohort\assignments\week-5\solution\backend> npm run dev ``` start building. #### Frontend Setup - go inside week-5/frontend and run: - +``` +C:\Users\tavis\OneDrive\Desktop\cohort\assignments\week-5\solution\frontend> serve . +``` ### Reference UI: ![Image](https://utfs.io/f/A8JZzw0Laf9jdQzX4lrWunt9yxDYPKUZgv60iAroJbcMF5RN) diff --git a/week-5/package-lock.json b/week-5/package-lock.json new file mode 100644 index 000000000..493386486 --- /dev/null +++ b/week-5/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "week-5", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/week-5/solution/backend/middleware/user.js b/week-5/solution/backend/middleware/user.js index 4ed77ad15..404fa1e60 100644 --- a/week-5/solution/backend/middleware/user.js +++ b/week-5/solution/backend/middleware/user.js @@ -2,6 +2,12 @@ const jwt = require('jsonwebtoken'); const SECRET = process.env.SECRET || 'secret000'; const authenticateJwt = (req, res, next) => { + /* + receive req.headers from script.js (submit button of todo) + { + Authorization: "Bearer abcdefghijklmnopqrstuvwxyz" (from local storage) + } + */ const authHeader = req.headers.authorization; if (authHeader) { const token = authHeader.split(' ')[1]; diff --git a/week-5/solution/backend/package-lock.json b/week-5/solution/backend/package-lock.json index 8598f9788..20c22c522 100644 --- a/week-5/solution/backend/package-lock.json +++ b/week-5/solution/backend/package-lock.json @@ -9,17 +9,22 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.0", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.6.3" + "mongoose": "^8.20.1", + "zod": "^4.1.13" + }, + "devDependencies": { + "nodemon": "^3.1.11" } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", - "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", + "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" @@ -53,12 +58,60 @@ "node": ">= 0.6" } }, + "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/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "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/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "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/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -82,10 +135,34 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "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/bson": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", - "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", "license": "Apache-2.0", "engines": { "node": ">=16.20.1" @@ -122,6 +199,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -315,6 +424,19 @@ "node": ">= 0.10.0" } }, + "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/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -349,6 +471,21 @@ "node": ">= 0.6" } }, + "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/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -375,6 +512,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -386,6 +536,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -456,6 +616,13 @@ "node": ">=0.10.0" } }, + "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -470,6 +637,52 @@ "node": ">= 0.10" } }, + "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/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -622,26 +835,39 @@ "node": ">= 0.6" } }, + "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/mongodb": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.8.0.tgz", - "integrity": "sha512-HGQ9NWDle5WvwMnrvUxsFYPd3JEbqD3RgABHBQRuoCEND0qzhsd0iH5ypHsf1eJ+sXmvmyKpP+FLOKY8Il7jMw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", "license": "Apache-2.0", "dependencies": { - "@mongodb-js/saslprep": "^1.1.5", - "bson": "^6.7.0", - "mongodb-connection-string-url": "^3.0.0" + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" }, "engines": { "node": ">=16.20.1" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", + "snappy": "^7.3.2", "socks": "^2.7.1" }, "peerDependenciesMeta": { @@ -669,23 +895,24 @@ } }, "node_modules/mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", "license": "Apache-2.0", "dependencies": { "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" + "whatwg-url": "^14.1.0 || ^13.0.0" } }, "node_modules/mongoose": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.3.tgz", - "integrity": "sha512-++yRmm7hjMbqVA/8WeiygTnEfrFbiy+OBjQi49GFJIvCQuSYE56myyQWo4j5hbpcHjhHQU8NukMNGTwAWFWjIw==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.20.1.tgz", + "integrity": "sha512-G+n3maddlqkQrP1nXxsI0q20144OSo+pe+HzRRGqaC4yK3FLYKqejqB9cbIi+SX7eoRsnG23LHGYNp8n7mWL2Q==", + "license": "MIT", "dependencies": { - "bson": "^6.7.0", + "bson": "^6.10.4", "kareem": "2.6.3", - "mongodb": "6.8.0", + "mongodb": "~6.20.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -763,6 +990,90 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "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/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/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/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/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -806,6 +1117,19 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, + "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/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -819,6 +1143,13 @@ "node": ">= 0.10" } }, + "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/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -864,6 +1195,19 @@ "node": ">= 0.8" } }, + "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/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -994,6 +1338,19 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, + "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/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -1011,6 +1368,32 @@ "node": ">= 0.8" } }, + "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/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1019,16 +1402,26 @@ "node": ">=0.6" } }, + "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/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "license": "MIT", "dependencies": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/type-is": { @@ -1043,6 +1436,13 @@ "node": ">= 0.6" } }, + "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" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1079,16 +1479,25 @@ } }, "node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { - "tr46": "^4.1.1", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" } } } diff --git a/week-5/solution/backend/package.json b/week-5/solution/backend/package.json index 006b08316..ca7d5459b 100644 --- a/week-5/solution/backend/package.json +++ b/week-5/solution/backend/package.json @@ -3,17 +3,23 @@ "version": "1.0.0", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node index.js", + "dev": "nodemon index.js" }, "keywords": [], "author": "", "license": "ISC", "description": "", "dependencies": { + "bcrypt": "^6.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.0", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.6.3" + "mongoose": "^8.20.1", + "zod": "^4.1.13" + }, + "devDependencies": { + "nodemon": "^3.1.11" } } diff --git a/week-5/solution/backend/routes/todo.js b/week-5/solution/backend/routes/todo.js index cd8671c59..a9ed70fa7 100644 --- a/week-5/solution/backend/routes/todo.js +++ b/week-5/solution/backend/routes/todo.js @@ -3,11 +3,19 @@ const { Todo } = require("../db"); const { authenticateJwt } = require("../middleware/user"); const router = express.Router(); +// authenticateJwt middleware before every todo route router.use(authenticateJwt); router.post("/", async (req, res) => { + /* + receive req.body from script.js (submit button of todo) + { + title: "go to gym" (from todo-input input box) + } + */ const createPayload = req.body; console.log(req.userId); + // req.userId from authenticateJwt middleware if (!createPayload.title) { return res.status(400).json({ @@ -36,6 +44,7 @@ router.post("/", async (req, res) => { }); router.get("/", async (req, res) => { + // req.userId from authenticateJwt middleware try { const todos = await Todo.find({ userId: req.userId }); diff --git a/week-5/solution/backend/routes/user.js b/week-5/solution/backend/routes/user.js index d058e32a7..0525cace8 100644 --- a/week-5/solution/backend/routes/user.js +++ b/week-5/solution/backend/routes/user.js @@ -1,40 +1,76 @@ const express = require('express'); const jwt = require('jsonwebtoken'); +const bcrypt = require('bcrypt'); +const { z } = require('zod'); const { authenticateJwt, SECRET } = require("../middleware/user"); const { User } = require("../db"); const router = express.Router(); +//const app = express() +// app.use(express.json()) router.post('/signup', async (req, res) => { + const requiredBody = z.object({ + username: z.email(), + password: z.string().min(3).max(30) + }) + const result = requiredBody.safeParse(req.body) + + if (!result.success) { + res.json({ + message: "Incorrect format: " + result.error.issues[0].message + }) + return + } + /* + receive req.body from script.js (submit button of signup) + { + username: username (from signup-username), + password: password (from signup-password) + } + */ const { username, password } = req.body; + // check if username already exists try { const user = await User.findOne({ username }); if (user) { return res.status(403).json({ message: 'User already exists' }); } - const newUser = new User({ username, password }); + const hashedPassword = await bcrypt.hash(password, 5); + const newUser = new User({ username, password: hashedPassword }); await newUser.save(); + /* + const user = await User.create({ + username: username, + password: password + }) + */ const token = jwt.sign({ userId: newUser._id }, SECRET, { expiresIn: '1h' }); res.json({ message: 'User created successfully', token }); } catch (error) { - res.status(500).json({ message: 'Error creating user', error }); + res.status(500).json({ message: 'Error creating user', error: error.message }); } }); router.post('/signin', async (req, res) => { const { username, password } = req.body; try { - const user = await User.findOne({ username, password }); - if (user) { - const token = jwt.sign({ userId: user._id }, SECRET, { expiresIn: '1h' }); + const user = await User.findOne({ username }); + if (!user) { + return res.status(403).json({ message: "Invalid username" }); + } - res.json({ message: 'Logged in successfully', token }); - } else { - res.status(403).json({ message: 'Invalid username or password' }); + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + return res.status(403).json({ message: "Invalid password" }); } + + const token = jwt.sign({ userId: user._id }, SECRET, { expiresIn: "1h" }); + res.json({ message: "Logged in successfully", token }); + } catch (error) { - res.status(500).json({ message: 'Error signing in', error }); + res.status(500).json({ message: "Error signing in", error: error.message }); } }); diff --git a/week-5/solution/frontend/index.html b/week-5/solution/frontend/index.html index a64cb0435..ee3e65775 100644 --- a/week-5/solution/frontend/index.html +++ b/week-5/solution/frontend/index.html @@ -20,9 +20,16 @@

Signup

+

Already have an account? Sign In +

@@ -34,9 +41,16 @@

Signin

+

Don't have an account? Sign Up +

@@ -46,8 +60,10 @@

Your Todo List

- - + + diff --git a/week-5/solution/frontend/package-lock.json b/week-5/solution/frontend/package-lock.json new file mode 100644 index 000000000..65e317057 --- /dev/null +++ b/week-5/solution/frontend/package-lock.json @@ -0,0 +1,46 @@ +{ + "name": "frontend", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "bcrypt": "^6.0.0" + } + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + } + } +} diff --git a/week-5/solution/frontend/package.json b/week-5/solution/frontend/package.json new file mode 100644 index 000000000..8da26fb6d --- /dev/null +++ b/week-5/solution/frontend/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "bcrypt": "^6.0.0" + } +} diff --git a/week-5/solution/frontend/script.js b/week-5/solution/frontend/script.js index 0ad89d693..c19466473 100644 --- a/week-5/solution/frontend/script.js +++ b/week-5/solution/frontend/script.js @@ -11,19 +11,34 @@ document.getElementById('signup-form').addEventListener('submit', async (e) => { const password = document.getElementById('signup-password').value; try { - const response = await fetch('http://localhost:3000/user/signup', { + const response = await fetch('http://localhost:3003/user/signup', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username, password }), + /* + send req.body to http://localhost:3003/user/signup + { + username: username (from signup-username), + password: password (from signup-password) + } + */ }); const result = await response.json(); + /* + receive response.json() and response.status() from http://localhost:3003/user/signup + { + "message": "User created successfully/User already exists/Error creating user", + "token": "abcdefghijklmnopqrstuvwxyz" + } + */ isSigningUp = false; if (response.ok) { document.getElementById('response-message').innerText = result.message || 'Signup successful, please sign in'; document.getElementById('signup-container').style.display = 'none'; + // The CSS property display: none; is used to completely hide an HTML element from the document. document.getElementById('signin-container').style.display = 'block'; } else { document.getElementById('response-message').innerText = result.message || 'Signup failed'; @@ -43,21 +58,36 @@ document.getElementById('signin-form').addEventListener('submit', async (e) => { const password = document.getElementById('signin-password').value; try { - const response = await fetch('http://localhost:3000/user/signin', { + const response = await fetch('http://localhost:3003/user/signin', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username, password }), }); + /* + send req.body to http://localhost:3003/user/signup + { + username: username (from signup-username), + password: password (from signup-password) + } + */ const result = await response.json(); + /* + response.json() + { + "message": "Logged in successfully/Invalid username or password/Error signing in", + "token": "abcdefghijklmnopqrstuvwxyz" + } + */ if (response.ok) { localStorage.setItem('token', result.token); document.getElementById('signin-container').style.display = 'none'; + // The CSS property display: none; is used to completely hide an HTML element from the document. document.getElementById('todo-container').style.display = 'block'; document.getElementById('response-message').innerHTML = - `Logged in successfully. Logout`; + `Logged in successfully.`; loadTodos(); // Add event listener for the logout link @@ -65,6 +95,7 @@ document.getElementById('signin-form').addEventListener('submit', async (e) => { e.preventDefault(); localStorage.removeItem('token'); // Clear token document.getElementById('todo-container').style.display = 'none'; + // The CSS property display: none; is used to completely hide an HTML element from the document. document.getElementById('signin-container').style.display = 'block'; document.getElementById('response-message').innerText = ''; }); @@ -92,8 +123,9 @@ document.getElementById('todo-form').addEventListener('submit', async (e) => { const token = localStorage.getItem('token'); + // router.post try { - const response = await fetch('http://localhost:3000/todo', { + const response = await fetch('http://localhost:3003/todo', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -101,11 +133,31 @@ document.getElementById('todo-form').addEventListener('submit', async (e) => { }, body: JSON.stringify({ title: todoText }), }); + /* + send req.headers to http://localhost:3003/user/todo + { + Authorization: "Bearer abcdefghijklmnopqrstuvwxyz" (from local storage) + } + send req.body to http://localhost:3003/user/todo + { + title: "go to gym" (from todo-input input box) + } + */ const result = await response.json(); + /* + response.json() + { + msg: "Todo created", + todo: newTodo where newTodo contains { + title: "go to gym", + completed: false, + userId: req.userId, + } + */ isAddingTodo = false; if (response.ok) { - todoInput.value = ''; + todoInput.value = ''; // clear todo input box loadTodos(); } else { console.error(result.msg); @@ -120,12 +172,19 @@ document.getElementById('todo-form').addEventListener('submit', async (e) => { async function loadTodos() { const token = localStorage.getItem('token'); try { - const response = await fetch('http://localhost:3000/todo', { + // router.get + const response = await fetch('http://localhost:3003/todo', { headers: { 'Authorization': `Bearer ${token}`, }, }); const { todos } = await response.json(); + /* + response.json() + { + todos: todos + } + */ const todoList = document.getElementById('todo-list'); todoList.innerHTML = ''; @@ -158,7 +217,7 @@ async function loadTodos() { async function completeTodo(id, completed) { const token = localStorage.getItem('token'); try { - await fetch(`http://localhost:3000/todo/${id}`, { + await fetch(`http://localhost:3003/todo/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -166,6 +225,16 @@ async function completeTodo(id, completed) { }, body: JSON.stringify({ completed }), }); + /* + send req.headers to http://localhost:3003/user/todo/{todo._id} + { + Authorization: "Bearer abcdefghijklmnopqrstuvwxyz" (from local storage) + } + send req.body to http://localhost:3003/user/todo/{todo._id} + { + completed: !todo.completed + } + */ loadTodos(); } catch (error) { console.error('Error completing todo:', error); @@ -176,11 +245,13 @@ async function completeTodo(id, completed) { document.getElementById('show-signin').addEventListener('click', (e) => { e.preventDefault(); document.getElementById('signup-container').style.display = 'none'; + // The CSS property display: none; is used to completely hide an HTML element from the document. document.getElementById('signin-container').style.display = 'block'; }); document.getElementById('show-signup').addEventListener('click', (e) => { e.preventDefault(); document.getElementById('signin-container').style.display = 'none'; + // The CSS property display: none; is used to completely hide an HTML element from the document. document.getElementById('signup-container').style.display = 'block'; }); diff --git a/week-5/solution/package-lock.json b/week-5/solution/package-lock.json new file mode 100644 index 000000000..c7fa1c889 --- /dev/null +++ b/week-5/solution/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "solution", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/week-7/client/src/App.jsx b/week-7/client/src/App.jsx index 8cc387045..9a24d9666 100644 --- a/week-7/client/src/App.jsx +++ b/week-7/client/src/App.jsx @@ -1,13 +1,52 @@ -// firstly, Don't get overwhelmed and if you are then go with client-easy. -import Home from "./pages/Home" +import React, { useState, useEffect } from "react"; +import Home from "./pages/Home"; +import AdminHome from "./pages/AdminHome"; + function App() { + const [isAdminView, setIsAdminView] = useState(false); + + // Automatically set view based on login state + useEffect(() => { + const adminToken = localStorage.getItem("adminToken"); + const userToken = localStorage.getItem("token"); + + if (adminToken) { + setIsAdminView(true); // admin logged in → show admin view + } else if (userToken) { + setIsAdminView(false); // user logged in → show user view + } + }, []); // runs only once when app loads + + const toggleView = () => { + setIsAdminView(prev => !prev); + }; return ( - <> - {/* start writing from here */} - - - ) +
+

Online Courses Platform

+ + {/* Toggle button */} + + + {/* Render view */} +
+ {isAdminView ? : } +
+
+ ); } -export default App +export default App; \ No newline at end of file diff --git a/week-7/client/src/components/AdminCourses.jsx b/week-7/client/src/components/AdminCourses.jsx new file mode 100644 index 000000000..3fe8ac127 --- /dev/null +++ b/week-7/client/src/components/AdminCourses.jsx @@ -0,0 +1,208 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; + +const AdminCourses = () => { + const [courses, setCourses] = useState([]); + const [message, setMessage] = useState(""); + const [form, setForm] = useState({ + title: "", + description: "", + imageUrl: "", + price: "", + }); + const [editingCourseId, setEditingCourseId] = useState(null); + + const token = localStorage.getItem("adminToken"); + + useEffect(() => { + fetchCourses(); + }, []); + + const fetchCourses = async () => { + if (!token) return; + try { + const res = await axios.get("http://localhost:3000/admin/courses", { + headers: { Authorization: `Bearer ${token}` }, + }); + setCourses(res.data.courses); + } catch (err) { + console.error(err); + } + }; + + const handleChange = (e) => { + setForm({ ...form, [e.target.name]: e.target.value }); + }; + + const createCourse = async () => { + if (!token) return setMessage("Please login as admin"); + try { + const res = await axios.post( + "http://localhost:3000/admin/courses", + form, + { headers: { Authorization: `Bearer ${token}` } } + ); + setMessage(res.data.message); + setForm({ title: "", description: "", imageUrl: "", price: "" }); + fetchCourses(); + } catch (err) { + setMessage(err.response?.data?.message || "Error creating course"); + } + }; + + const startEdit = (course) => { + setEditingCourseId(course._id); + setForm({ + title: course.title, + description: course.description, + imageUrl: course.imageUrl, + price: course.price, + }); + }; + + const updateCourse = async () => { + if (!token || !editingCourseId) return; + try { + const res = await axios.put( + `http://localhost:3000/admin/courses/${editingCourseId}`, + form, + { headers: { Authorization: `Bearer ${token}` } } + ); + setMessage(res.data.message); + setEditingCourseId(null); + setForm({ title: "", description: "", imageUrl: "", price: "" }); + fetchCourses(); + } catch (err) { + setMessage(err.response?.data?.message || "Error updating course"); + } + }; + + return ( +
+

Admin Dashboard

+ {message &&

{message}

} + +

+ {editingCourseId ? "Edit Course" : "Create Course"} +

+
+ + + + + {editingCourseId ? ( + + ) : ( + + )} +
+ +

Your Courses

+ +
+ ); +}; + +const styles = { + container: { + border: "1px solid #ccc", + padding: "20px", + borderRadius: "8px", + maxWidth: "600px", + margin: "0 auto", + backgroundColor: "#f9f9f9", + }, + heading: { + textAlign: "center", + color: "#333", + }, + subHeading: { + marginTop: "20px", + color: "#555", + }, + message: { + color: "green", + fontWeight: "bold", + }, + form: { + display: "flex", + flexDirection: "column", + gap: "10px", + marginBottom: "20px", + }, + input: { + padding: "8px", + borderRadius: "5px", + border: "1px solid #ccc", + fontSize: "14px", + }, + button: { + padding: "10px", + backgroundColor: "#4caf50", + color: "white", + border: "none", + borderRadius: "5px", + cursor: "pointer", + fontWeight: "bold", + }, + courseList: { + listStyleType: "none", + padding: 0, + }, + courseItem: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "8px 0", + borderBottom: "1px solid #ddd", + }, + editButton: { + padding: "5px 10px", + backgroundColor: "#2196f3", + color: "white", + border: "none", + borderRadius: "4px", + cursor: "pointer", + }, +}; + +export default AdminCourses; \ No newline at end of file diff --git a/week-7/client/src/components/AdminLogin.jsx b/week-7/client/src/components/AdminLogin.jsx new file mode 100644 index 000000000..94bde31be --- /dev/null +++ b/week-7/client/src/components/AdminLogin.jsx @@ -0,0 +1,89 @@ +import React, { useState } from "react"; +import axios from "axios"; + +const AdminLogin = () => { + const [formData, setFormData] = useState({ + email: "", + password: "" + }); + + const [message, setMessage] = useState(""); + + function handleChange(e) { + setFormData({ ...formData, [e.target.name]: e.target.value }); + } + + async function handleLogin() { + try { + const response = await axios.post( + "http://localhost:3000/admin/login", + formData + ); + + setMessage(response.data.message); + + if (response.data.token) { + localStorage.setItem("adminToken", response.data.token); + window.location.reload(); // simple way to refresh Home.jsx state + } + } catch (error) { + setMessage(error.response?.data?.message || "Login failed"); + } + } + + return ( +
+

Login

+ + + + + + + + {message &&

{message}

} +
+ ); +}; + +const styles = { + container: { + display: "flex", + flexDirection: "column", + gap: "10px", + width: "100%", + marginTop: "10px", + }, + input: { + padding: "10px", + border: "1px solid #ccc", + borderRadius: "5px", + }, + button: { + padding: "10px", + background: "#2196F3", + color: "white", + border: "none", + borderRadius: "5px", + cursor: "pointer", + fontWeight: "bold", + } +}; + +export default AdminLogin; \ No newline at end of file diff --git a/week-7/client/src/components/AdminRegister.jsx b/week-7/client/src/components/AdminRegister.jsx new file mode 100644 index 000000000..a7a7cee2d --- /dev/null +++ b/week-7/client/src/components/AdminRegister.jsx @@ -0,0 +1,106 @@ +import React, { useState } from "react"; +import axios from "axios"; + +const AdminRegister = () => { + const [formData, setFormData] = useState({ + email: "", + password: "", + firstName: "", + lastName: "", + }); + + const [message, setMessage] = useState(""); + + function handleChange(e) { + setFormData({ ...formData, [e.target.name]: e.target.value }); + } + + async function handleRegister() { + try { + const response = await axios.post("http://localhost:3000/admin/signup", formData); + + setMessage(response.data.message); + + if (response.data.token) { + localStorage.setItem("adminToken", response.data.token); + window.location.reload(); // simple way to refresh Home.jsx state + } + } catch (error) { + setMessage(error.response?.data?.message || "Error occurred"); + } + } + + return ( +
+

Create Account

+ + + + + + + + + + + + {message &&

{message}

} +
+ ); +}; + +const styles = { + container: { + display: "flex", + flexDirection: "column", + gap: "10px", + width: "100%", + marginTop: "10px", + }, + input: { + padding: "10px", + border: "1px solid #ccc", + borderRadius: "5px", + }, + button: { + padding: "10px", + background: "#4CAF50", + color: "white", + border: "none", + borderRadius: "5px", + cursor: "pointer", + fontWeight: "bold", + }, +}; + +export default AdminRegister; \ No newline at end of file diff --git a/week-7/client/src/components/Courses.jsx b/week-7/client/src/components/Courses.jsx index cfbbaee98..a15d5a1f6 100644 --- a/week-7/client/src/components/Courses.jsx +++ b/week-7/client/src/components/Courses.jsx @@ -1,11 +1,118 @@ -// courses code here -import React from 'react' +import React, { useEffect, useState } from "react"; +import axios from "axios"; -// use axios here, similar to register and login const Courses = () => { + const [courses, setCourses] = useState([]); + const [purchased, setPurchased] = useState([]); + const [message, setMessage] = useState(""); + + // get token from localStorage + const token = localStorage.getItem("token"); + + // fetch all courses + const fetchCourses = async () => { + try { + const res = await axios.get("http://localhost:3000/users/courses"); + setCourses(res.data.courses); + } catch (err) { + console.error("Error fetching courses:", err); + } + }; + + // fetch purchased courses + const fetchPurchasedCourses = async () => { + if (!token) return; + try { + const res = await axios.get("http://localhost:3000/users/purchasedCourses", { + headers: { Authorization: `Bearer ${token}` }, + }); + setPurchased(res.data.coursesData.map(c => c._id)); + } catch (err) { + console.error("Error fetching purchased courses:", err); + } + }; + + useEffect(() => { + fetchCourses(); + fetchPurchasedCourses(); + }, []); + + // purchase a course + const purchaseCourse = async (courseId) => { + if (!token) { + setMessage("Please login to purchase courses"); + return; + } + + try { + const res = await axios.post( + `http://localhost:3000/users/courses/${courseId}`, + {}, + { headers: { Authorization: `Bearer ${token}` } } + ); + setMessage(res.data.message); + setPurchased([...purchased, courseId]); + } catch (err) { + setMessage(err.response?.data?.message || "Purchase failed"); + } + }; + return ( -
Courses
- ) -} +
+

Available Courses

+ {message &&

{message}

} +
+ {courses.map(course => ( +
+

{course.title}

+

{course.description}

+

Price: ${course.price}

+ {purchased.includes(course._id) ? ( + + ) : ( + + )} +
+ ))} +
+
+ ); +}; + +const styles = { + courseContainer: { + display: "flex", + gap: "20px", + flexWrap: "wrap", + }, + courseCard: { + border: "1px solid #ccc", + borderRadius: "10px", + padding: "15px", + width: "250px", + }, + purchaseButton: { + background: "#4CAF50", + color: "white", + border: "none", + padding: "8px 12px", + borderRadius: "5px", + cursor: "pointer", + }, + purchasedButton: { + background: "#aaa", + color: "white", + border: "none", + padding: "8px 12px", + borderRadius: "5px", + }, +}; -export default Courses \ No newline at end of file +export default Courses; \ No newline at end of file diff --git a/week-7/client/src/components/Login.jsx b/week-7/client/src/components/Login.jsx index 0948cf6e0..08804aaad 100644 --- a/week-7/client/src/components/Login.jsx +++ b/week-7/client/src/components/Login.jsx @@ -1,15 +1,89 @@ -// login code here -import axios from 'axios'; -import React from 'react' +import React, { useState } from "react"; +import axios from "axios"; const Login = () => { - // call the functions onClick of button. - async function handleLogin() { - const resposne = await axios.post(); // // if you don't know about axios, give it a read https://axios-http.com/docs/intro + const [formData, setFormData] = useState({ + email: "", + password: "" + }); + + const [message, setMessage] = useState(""); + + function handleChange(e) { + setFormData({ ...formData, [e.target.name]: e.target.value }); + } + + async function handleLogin() { + try { + const response = await axios.post( + "http://localhost:3000/users/login", + formData + ); + + setMessage(response.data.message); + + if (response.data.token) { + localStorage.setItem("token", response.data.token); + window.location.reload(); // simple way to refresh Home.jsx state + } + } catch (error) { + setMessage(error.response?.data?.message || "Login failed"); } - return ( -
Login
- ) -} + } + + return ( +
+

Login

+ + + + + + + + {message &&

{message}

} +
+ ); +}; + +const styles = { + container: { + display: "flex", + flexDirection: "column", + gap: "10px", + width: "100%", + marginTop: "10px", + }, + input: { + padding: "10px", + border: "1px solid #ccc", + borderRadius: "5px", + }, + button: { + padding: "10px", + background: "#2196F3", + color: "white", + border: "none", + borderRadius: "5px", + cursor: "pointer", + fontWeight: "bold", + } +}; -export default Login \ No newline at end of file +export default Login; \ No newline at end of file diff --git a/week-7/client/src/components/Register.jsx b/week-7/client/src/components/Register.jsx index c143c3f6e..f5528ac97 100644 --- a/week-7/client/src/components/Register.jsx +++ b/week-7/client/src/components/Register.jsx @@ -1,16 +1,106 @@ -// register code here -import React from 'react' -import axios from "axios" - +import React, { useState } from "react"; +import axios from "axios"; const Register = () => { - // call the functions onClick of button. - async function handleRegister() { - const resposne = await axios.post(); // if you don't know about axios, give it a read https://axios-http.com/docs/intro + const [formData, setFormData] = useState({ + email: "", + password: "", + firstName: "", + lastName: "", + }); + + const [message, setMessage] = useState(""); + + function handleChange(e) { + setFormData({ ...formData, [e.target.name]: e.target.value }); + } + + async function handleRegister() { + try { + const response = await axios.post("http://localhost:3000/users/signup", formData); + + setMessage(response.data.message); + + if (response.data.token) { + localStorage.setItem("token", response.data.token); + window.location.reload(); // simple way to refresh Home.jsx state } - return ( -
Register
- ) -} + } catch (error) { + setMessage(error.response?.data?.message || "Error occurred"); + } + } + + return ( +
+

Create Account

+ + + + + + + + + + + + {message &&

{message}

} +
+ ); +}; + +const styles = { + container: { + display: "flex", + flexDirection: "column", + gap: "10px", + width: "100%", + marginTop: "10px", + }, + input: { + padding: "10px", + border: "1px solid #ccc", + borderRadius: "5px", + }, + button: { + padding: "10px", + background: "#4CAF50", + color: "white", + border: "none", + borderRadius: "5px", + cursor: "pointer", + fontWeight: "bold", + }, +}; -export default Register \ No newline at end of file +export default Register; \ No newline at end of file diff --git a/week-7/client/src/pages/AdminHome.jsx b/week-7/client/src/pages/AdminHome.jsx new file mode 100644 index 000000000..d10ce15d1 --- /dev/null +++ b/week-7/client/src/pages/AdminHome.jsx @@ -0,0 +1,71 @@ +import React, { useState, useEffect } from "react"; +import AdminLogin from "../components/AdminLogin"; +import AdminRegister from "../components/AdminRegister"; +import AdminCourses from "../components/AdminCourses"; + +const AdminHome = () => { + const [isLoggedIn, setIsLoggedIn] = useState(false); + + useEffect(() => { + const token = localStorage.getItem("adminToken"); + setIsLoggedIn(!!token); + }, []); + /* + Runs once when the component loads. + Looks for adminToken in localStorage. + If it exists → isLoggedIn = true + If not → isLoggedIn = false + */ + + const handleLogout = () => { + localStorage.removeItem("adminToken"); + setIsLoggedIn(false); + }; + /* + Removes the admin token from local storage. + Updates isLoggedIn to false, which re-renders the component and shows login/register forms again. + */ + + /* + Admin logged in (isLoggedIn === true): + Logout button + AdminCourses component (to manage or view courses) + + Admin not logged in (isLoggedIn === false): + AdminRegister and AdminLogin forms side by side + */ + return ( +
+

Admin Dashboard

+ + {isLoggedIn ? ( +
+ + +
+ ) : ( +
+ + +
+ )} +
+ ); +}; + +const styles = { + authContainer: { display: "flex", gap: "20px" }, + logoutButton: { + padding: "8px 12px", + background: "#f44336", + color: "white", + border: "none", + borderRadius: "5px", + cursor: "pointer", + marginBottom: "15px", + }, +}; + +export default AdminHome; \ No newline at end of file diff --git a/week-7/client/src/pages/Home.jsx b/week-7/client/src/pages/Home.jsx index 94691be96..8e7624364 100644 --- a/week-7/client/src/pages/Home.jsx +++ b/week-7/client/src/pages/Home.jsx @@ -1,16 +1,79 @@ -// implement the home page UI here. -import React from 'react' - -// compoents imports -import Login from '../components/Login' -import Register from '../components/Register' -import Courses from '../components/Courses' +import React, { useState, useEffect } from "react"; +import Login from "../components/Login"; +import Register from "../components/Register"; +import Courses from "../components/Courses"; const Home = () => { + const [isLoggedIn, setIsLoggedIn] = useState(false); + // isLoggedIn state keeps track of whether the user is logged in. + // Initially, it is false (user is not logged in). + + useEffect(() => { + const token = localStorage.getItem("token"); + setIsLoggedIn(!!token); + }, []); + /* + useEffect runs once when the component mounts ([] dependency array). + Checks localStorage for a token: + If a token exists → user is considered logged in → isLoggedIn becomes true + If no token → isLoggedIn remains false + */ + + const handleLogout = () => { + localStorage.removeItem("token"); + setIsLoggedIn(false); + }; + /* + Removes the token from local storage. + Sets isLoggedIn to false, which will re-render the component and show the login/register forms again. + */ + + /* + Logged in (isLoggedIn === true): + Shows a Logout button + Shows the component (user can view courses) + + Not logged in (isLoggedIn === false): + Shows Register and Login forms side by side (styled with flex) + */ return ( - // write home page UI code here -
Home
- ) -} +
+

User Dashboard

+ + {isLoggedIn ? ( +
+ + +
+ ) : ( + <> +
+ + +
+
+ {/* Public Courses view */} + {/* fetchCourses() inside Courses will load courses from /users/courses */} +
+ + )} +
+ ); +}; + +const styles = { + authContainer: { display: "flex", gap: "20px" }, + logoutButton: { + padding: "8px 12px", + background: "#f44336", + color: "white", + border: "none", + borderRadius: "5px", + cursor: "pointer", + marginBottom: "15px", + }, +}; -export default Home \ No newline at end of file +export default Home; \ No newline at end of file diff --git a/week-7/server/.env.example b/week-7/server/.env.example index 6c797aa93..780972744 100644 --- a/week-7/server/.env.example +++ b/week-7/server/.env.example @@ -1,2 +1,6 @@ -JWT_SECRET="" -PORT= \ No newline at end of file +JWT_USER_SECRET="" +JWT_ADMIN_SECRET="" +PORT= +MONGO_URI=mongodb://localhost:27017/coursera-app +// netstat -ano | findstr :3000 +// taskkill /PID 12345 /F \ No newline at end of file diff --git a/week-7/server/db/db.js b/week-7/server/db/db.js new file mode 100644 index 000000000..428d628a1 --- /dev/null +++ b/week-7/server/db/db.js @@ -0,0 +1,51 @@ +// Define mongoose schemas + +const mongoose = require("mongoose"); +const ObjectId = mongoose.Types.ObjectId; + +const userSchema = new mongoose.Schema({ + // userSchema here + email: { type: String, unique: true }, + password: String, + firstName: String, + lastName: String, +}); + +const adminSchema = new mongoose.Schema({ + // adminSchema here + email: { type: String, unique: true }, + password: String, + firstName: String, + lastName: String, +}); + +const courseSchema = new mongoose.Schema({ + // courseSchema here + title: String, + description: String, + price: Number, + imageUrl: String, + creatorId: ObjectId, +}); + +const purchaseSchema = new mongoose.Schema({ + userId: ObjectId, + courseId: ObjectId, +}); + +// Define mongoose models +const User = mongoose.model("User", userSchema); +const Admin = mongoose.model("Admin", adminSchema); +const Course = mongoose.model("Course", courseSchema); +const Purchase = mongoose.model("Purchase", purchaseSchema); + +const authMiddleware = (req, res, next) => { + // authMiddleware logic here +}; + +module.exports = { + User, + Admin, + Course, + Purchase, +}; diff --git a/week-7/server/middleware/admin.js b/week-7/server/middleware/admin.js new file mode 100644 index 000000000..e050af04b --- /dev/null +++ b/week-7/server/middleware/admin.js @@ -0,0 +1,29 @@ +const jwt = require("jsonwebtoken"); +const SECRET = process.env.JWT_ADMIN_SECRET || "secret000"; + +const authenticateAdminJwt = (req, res, next) => { + /* + receive req.headers from script.js (submit button of todo) + { + Authorization: "Bearer abcdefghijklmnopqrstuvwxyz" (from local storage) + } + */ + const authHeader = req.headers.authorization; + if (authHeader) { + const token = authHeader.split(" ")[1]; + jwt.verify(token, SECRET, (err, user) => { + if (err) { + return res.status(403).json({ message: "Forbidden: Invalid token" }); + } + req.userId = user.userId; + next(); + }); + } else { + res.status(401).json({ message: "Unauthorized: No token provided" }); + } +}; + +module.exports = { + authenticateAdminJwt, + SECRET, +}; diff --git a/week-7/server/middleware/user.js b/week-7/server/middleware/user.js new file mode 100644 index 000000000..2fc546aa5 --- /dev/null +++ b/week-7/server/middleware/user.js @@ -0,0 +1,29 @@ +const jwt = require("jsonwebtoken"); +const SECRET = process.env.JWT_USER_SECRET || "secret000"; + +const authenticateUserJwt = (req, res, next) => { + /* + receive req.headers from script.js (submit button of todo) + { + Authorization: "Bearer abcdefghijklmnopqrstuvwxyz" (from local storage) + } + */ + const authHeader = req.headers.authorization; + if (authHeader) { + const token = authHeader.split(" ")[1]; + jwt.verify(token, SECRET, (err, user) => { + if (err) { + return res.status(403).json({ message: "Forbidden: Invalid token" }); + } + req.userId = user.userId; + next(); + }); + } else { + res.status(401).json({ message: "Unauthorized: No token provided" }); + } +}; + +module.exports = { + authenticateUserJwt, + SECRET, +}; diff --git a/week-7/server/package-lock.json b/week-7/server/package-lock.json index 5f1a5b874..6ff61a60f 100644 --- a/week-7/server/package-lock.json +++ b/week-7/server/package-lock.json @@ -9,17 +9,19 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.21.0", - "jsonwebtoken": "^9.0.2", - "mongoose": "^8.6.2" + "express": "^4.22.1", + "jsonwebtoken": "^9.0.3", + "mongoose": "^8.6.2", + "zod": "^4.1.13" } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", - "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", + "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" @@ -59,6 +61,20 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -84,9 +100,9 @@ } }, "node_modules/bson": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", - "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", "license": "Apache-2.0", "engines": { "node": ">=16.20.1" @@ -107,17 +123,27 @@ "node": ">= 0.8" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -148,9 +174,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -184,23 +210,6 @@ "ms": "2.0.0" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -232,6 +241,20 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -257,13 +280,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -277,6 +297,18 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -293,45 +325,64 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/finalhandler": { @@ -380,16 +431,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -398,34 +454,23 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gopd": { + "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.4" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -435,9 +480,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -502,12 +547,12 @@ } }, "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "license": "MIT", "dependencies": { - "jws": "^3.2.2", + "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -530,23 +575,23 @@ "license": "MIT" }, "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -601,6 +646,15 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -668,25 +722,25 @@ } }, "node_modules/mongodb": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.8.0.tgz", - "integrity": "sha512-HGQ9NWDle5WvwMnrvUxsFYPd3JEbqD3RgABHBQRuoCEND0qzhsd0iH5ypHsf1eJ+sXmvmyKpP+FLOKY8Il7jMw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", "license": "Apache-2.0", "dependencies": { - "@mongodb-js/saslprep": "^1.1.5", - "bson": "^6.7.0", - "mongodb-connection-string-url": "^3.0.0" + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" }, "engines": { "node": ">=16.20.1" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", + "snappy": "^7.3.2", "socks": "^2.7.1" }, "peerDependenciesMeta": { @@ -714,24 +768,24 @@ } }, "node_modules/mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", "license": "Apache-2.0", "dependencies": { "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" + "whatwg-url": "^14.1.0 || ^13.0.0" } }, "node_modules/mongoose": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.2.tgz", - "integrity": "sha512-ErbDVvuUzUfyQpXvJ6sXznmZDICD8r6wIsa0VKjJtB6/LZncqwUn5Um040G1BaNo6L3Jz+xItLSwT0wZmSmUaQ==", + "version": "8.20.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.20.2.tgz", + "integrity": "sha512-U0TPupnqBOAI3p9H9qdShX8/nJUBylliRcHFKuhbewEkM7Y0qc9BbrQR9h4q6+1easoZqej7cq2Ee36AZ0gMzQ==", "license": "MIT", "dependencies": { - "bson": "^6.7.0", + "bson": "^6.10.4", "kareem": "2.6.3", - "mongodb": "6.8.0", + "mongodb": "~6.20.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -810,6 +864,26 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -820,9 +894,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -853,9 +927,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, "node_modules/proxy-addr": { @@ -1011,39 +1085,76 @@ "node": ">= 0.8.0" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { - "define-data-property": "^1.1.4", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -1086,15 +1197,15 @@ } }, "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "license": "MIT", "dependencies": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/type-is": { @@ -1147,16 +1258,25 @@ } }, "node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { - "tr46": "^4.1.1", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" } } } diff --git a/week-7/server/package.json b/week-7/server/package.json index ed08190c4..8d1b3b780 100644 --- a/week-7/server/package.json +++ b/week-7/server/package.json @@ -3,17 +3,20 @@ "version": "1.0.0", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node server.js", + "dev": "nodemon server.js" }, "keywords": [], "author": "", "license": "ISC", "description": "", "dependencies": { + "bcrypt": "^6.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.21.0", - "jsonwebtoken": "^9.0.2", - "mongoose": "^8.6.2" + "express": "^4.22.1", + "jsonwebtoken": "^9.0.3", + "mongoose": "^8.6.2", + "zod": "^4.1.13" } } diff --git a/week-7/server/routes/admin.js b/week-7/server/routes/admin.js new file mode 100644 index 000000000..5ffcf0cd4 --- /dev/null +++ b/week-7/server/routes/admin.js @@ -0,0 +1,185 @@ +// Admin routes + +const { Router } = require("express"); +const adminRouter = Router(); +const { Admin, Course } = require("../db/db"); +const { z } = require("zod"); +const bcrypt = require("bcrypt"); +const jwt = require("jsonwebtoken"); +const dotenv = require("dotenv"); +dotenv.config(); +const { SECRET, authenticateAdminJwt } = require("../middleware/admin"); + +adminRouter.post("/signup", async (req, res) => { + // logic to sign up admin + const requiredBody = z.object({ + email: z.email(), + password: z.string().min(3).max(30), + firstName: z.string(), + lastName: z.string(), + }); + const result = requiredBody.safeParse(req.body); + + if (!result.success) { + res.json({ + message: "Incorrect format: " + result.error.issues[0].message, + }); + return; + } + /* + receive req.body from script.js (submit button of signup) + { + email: email (from signup-email), + password: password (from signup-password), + firstName: firstName, + lastName: lastName + } + */ + const { email, password, firstName, lastName } = req.body; + // check if email already exists + try { + const user = await Admin.findOne({ email }); + if (user) { + return res.status(403).json({ message: "Admin already exists" }); + } + + const hashedPassword = await bcrypt.hash(password, 5); + const newAdmin = new Admin({ + email, + password: hashedPassword, + firstName, + lastName, + }); + await newAdmin.save(); + /* + const user = await Admin.create({ + email: email, + password: password, + firstName: firstName, + lastName, lastName + }) + */ + + const token = jwt.sign({ userId: newAdmin._id }, SECRET, { + expiresIn: "1h", + }); + res.json({ message: "Admin created successfully", token }); + } catch (error) { + res + .status(500) + .json({ message: "Error creating user", error: error.message }); + } +}); + +adminRouter.post("/login", async (req, res) => { + // logic to log in admin + const { email, password } = req.body; + try { + const user = await Admin.findOne({ email }); + if (!user) { + return res.status(403).json({ message: "Invalid email" }); + } + + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + return res.status(403).json({ message: "Invalid password" }); + } + + const token = jwt.sign({ userId: user._id }, SECRET, { expiresIn: "1h" }); + res.json({ message: "Logged in successfully", token }); + } catch (error) { + res.status(500).json({ message: "Error signing in", error: error.message }); + } +}); + +adminRouter.post("/courses", authenticateAdminJwt, async (req, res) => { + // logic to create a course + const adminId = req.userId; + const { title, description, imageUrl, price } = req.body; + + try { + const course = await Course.findOne({ title }); + if (course) { + return res.status(403).json({ message: "Course already exists" }); + } + + const newCourse = await Course.create({ + title: title, + description: description, + imageUrl: imageUrl, + price: price, + creatorId: adminId, + }); + + res.json({ + message: "Course created successfully", + courseId: newCourse._id, + }); + } catch (error) { + res + .status(500) + .json({ message: "Error creating course", error: error.message }); + } +}); + +adminRouter.put( + "/courses/:courseId", + authenticateAdminJwt, + async (req, res) => { + // logic to edit a course + const adminId = req.userId; + const courseId = req.params.courseId; + const { title, description, imageUrl, price } = req.body; + + const course = await Course.findOne({ + _id: courseId, + }); + + if (!course) { + return res.status(404).json({ + message: "Course not found", + }); + } + + const updatedCourse = await Course.findOneAndUpdate( + { + _id: courseId, + creatorId: adminId, + }, + { + title: title, + description: description, + imageUrl: imageUrl, + price: price, + }, + ); + + if (!updatedCourse) { + return res.status(404).json({ + message: "Course not found or not owned by admin", + }); + } + + res.json({ + message: "Course updated", + courseId: updatedCourse._id, + }); + }, +); + +adminRouter.get("/courses", authenticateAdminJwt, async (req, res) => { + // logic to get all courses + const adminId = req.userId; + + const courses = await Course.find({ + creatorId: adminId, + }); + + res.json({ + courses: courses, + }); +}); + +module.exports = { + adminRouter: adminRouter, +}; diff --git a/week-7/server/routes/user.js b/week-7/server/routes/user.js new file mode 100644 index 000000000..fb75e91cb --- /dev/null +++ b/week-7/server/routes/user.js @@ -0,0 +1,145 @@ +// User routes + +const { Router } = require("express"); +const userRouter = Router(); +const { User, Purchase, Course } = require("../db/db"); +const { z } = require('zod'); +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const dotenv = require("dotenv"); +dotenv.config(); +const { SECRET, authenticateUserJwt } = require("../middleware/user") + +userRouter.post('/signup', async (req, res) => { + // logic to sign up user + const requiredBody = z.object({ + email: z.email(), + password: z.string().min(3).max(30), + firstName: z.string(), + lastName: z.string() + }) + const result = requiredBody.safeParse(req.body) + + if (!result.success) { + res.json({ + message: "Incorrect format: " + result.error.issues[0].message + }) + return + } + /* + receive req.body from script.js (submit button of signup) + { + email: email (from signup-email), + password: password (from signup-password), + firstName: firstName, + lastName: lastName + } + */ + const { email, password, firstName, lastName } = req.body; + // check if email already exists + try { + const user = await User.findOne({ email }); + if (user) { + return res.status(403).json({ message: 'User already exists' }); + } + + const hashedPassword = await bcrypt.hash(password, 5); + const newUser = new User({ email, password: hashedPassword, firstName, lastName }); + await newUser.save(); + /* + const user = await User.create({ + email: email, + password: password, + firstName: firstName, + lastName, lastName + }) + */ + + const token = jwt.sign({ userId: newUser._id }, SECRET, { expiresIn: '1h' }); + res.json({ message: 'User created successfully', token }); + } catch (error) { + res.status(500).json({ message: 'Error creating user', error: error.message }); + } +}); + +userRouter.post('/login', async (req, res) => { + // logic to log in user + const { email, password } = req.body; + try { + const user = await User.findOne({ email }); + if (!user) { + return res.status(403).json({ message: "Invalid email" }); + } + + const isPasswordValid = await bcrypt.compare(password, user.password); + if (!isPasswordValid) { + return res.status(403).json({ message: "Invalid password" }); + } + + const token = jwt.sign({ userId: user._id }, SECRET, { expiresIn: "1h" }); + // Inspect -> Application -> Local Storage + res.json({ message: "Logged in successfully", token }); + + } catch (error) { + res.status(500).json({ message: "Error signing in", error: error.message }); + } +}); + +userRouter.get('/courses', async (req, res) => { + // logic to list all courses + const courses = await Course.find({}); + + res.json({ + courses: courses + }) +}); + +userRouter.post('/courses/:courseId', authenticateUserJwt, async (req, res) => { + // logic to purchase a course + const userId = req.userId + const courseId = req.params.courseId + + const course = await Course.findOne({ + _id: courseId + }); + + if (!course) { + return res.status(404).json({ + message: "Course not found" + }); + } + + try { + const newPurchase = await Purchase.create({ + userId: userId, + courseId: courseId + }) + + res.json({ message: 'Course purchased successfully', courseId: newPurchase._id }); + } catch (error) { + res.status(500).json({ message: 'Error creating course', error: error.message }); + } + +}); + +userRouter.get('/purchasedCourses', authenticateUserJwt, async (req, res) => { + // logic to view purchased courses + const userId = req.userId + + const purchasedCourses = await Purchase.find({ + userId: userId, + }); + + const coursesData = await Course.find({ + _id: { $in: purchasedCourses.map(x => x.courseId)} + }) + + res.json({ + courses: purchasedCourses, + coursesData: coursesData + }); +}); + +module.exports = { + userRouter: userRouter +} \ No newline at end of file diff --git a/week-7/server/server.js b/week-7/server/server.js index de3d9f638..1bc052317 100644 --- a/week-7/server/server.js +++ b/week-7/server/server.js @@ -3,82 +3,25 @@ const express = require('express'); const jwt = require('jsonwebtoken'); const mongoose = require('mongoose'); const dotenv = require("dotenv"); +const cors = require("cors"); +const { userRouter } = require('./routes/user'); +const { adminRouter } = require('./routes/admin'); +const { connectToDatabase } = require("./db/db"); dotenv.config(); +// Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env const app = express(); +app.use(cors()); app.use(express.json()); -const secret = process.env.JWT_SECRERT; // This should be in an environment variable in a real application -const port = process.env.PORT; +const secret = process.env.JWT_SECRET; // This should be in an environment variable in a real application +const PORT = process.env.PORT || 3000; -// Define mongoose schemas -const userSchema = new mongoose.Schema({ - // userSchema here -}); - -const adminSchema = new mongoose.Schema({ -// adminSchema here -}); - -const courseSchema = new mongoose.Schema({ -// courseSchema here -}); - -// Define mongoose models -const User = mongoose.model('User', userSchema); -const Admin = mongoose.model('Admin', adminSchema); -const Course = mongoose.model('Course', courseSchema); - -const authMiddleware = (req, res, next) => { -// authMiddleware logic here -}; - -// Connect to MongoDB -mongoose.connect(''); - - -// Admin routes -app.post('/admin/signup', (req, res) => { - // logic to sign up admin -}); - -app.post('/admin/login', (req, res) => { - // logic to log in admin -}); - -app.post('/admin/courses', (req, res) => { - // logic to create a course -}); - -app.put('/admin/courses/:courseId', (req, res) => { - // logic to edit a course -}); - -app.get('/admin/courses', (req, res) => { - // logic to get all courses -}); - -// User routes -app.post('/users/signup', (req, res) => { - // logic to sign up user -}); +app.use("/users", userRouter); +app.use("/admin", adminRouter); -app.post('/users/login', (req, res) => { - // logic to log in user +app.listen(PORT, () => { + // Connect to MongoDB + mongoose.connect(process.env.MONGO_URI); + console.log(`Server running on port ${PORT}`); }); - -app.get('/users/courses', (req, res) => { - // logic to list all courses -}); - -app.post('/users/courses/:courseId', (req, res) => { - // logic to purchase a course -}); - -app.get('/users/purchasedCourses', (req, res) => { - // logic to view purchased courses -}); - -app.listen(port, () => { - console.log('Server is listening on port 3000'); -}); \ No newline at end of file