From a03368714023956eaf01dc782ea5d0215a790504 Mon Sep 17 00:00:00 2001 From: GinaCastromonte <123599985+GinaCastromonte@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:56:49 -0400 Subject: [PATCH] reconstructed basic/advanced --- TDD-TESTING-GUIDE.md | 7 +- assignments/04-tasks-validations.md | 735 +++++++++++++----- lessons/04-tasks-validations.md | 623 ++++++++++++++- .../mentor-slides/04-tasks-validations.md | 424 +++++----- .../mentors-session-notes/lesson4.md | 221 ++++-- .../assignment4/validation/taskSchema.js | 10 +- .../assignment4/validation/userSchema.js | 9 +- 7 files changed, 1585 insertions(+), 444 deletions(-) diff --git a/TDD-TESTING-GUIDE.md b/TDD-TESTING-GUIDE.md index fb22f62..d31f4bc 100644 --- a/TDD-TESTING-GUIDE.md +++ b/TDD-TESTING-GUIDE.md @@ -67,12 +67,15 @@ Tests verify: #### **Assignment 4** (Tasks and Validations) ```bash -npm run tdd assignment4 +npm run tdd assignment4a +npm run tdd assignment4b # For optional advanced part ``` Tests verify: - User registration, login, and logoff - Task CRUD operations +- Protected task access and task ownership - Validation schemas work correctly +- Optional advanced validation, patch update, and password security checks #### **Assignment 5** (SQL Introduction) ```bash @@ -272,7 +275,7 @@ test('globals-demo.js outputs correct globals', () => { - Make sure it logs the exact strings the test expects - Pay attention to spacing and capitalization! -### Example: Reading assignment4.test.js +### Example: Reading assignment4a.test.js For controller tests (like Assignment 4), tests use `node-mocks-http`: diff --git a/assignments/04-tasks-validations.md b/assignments/04-tasks-validations.md index d98d199..45da6ff 100644 --- a/assignments/04-tasks-validations.md +++ b/assignments/04-tasks-validations.md @@ -1,306 +1,695 @@ -# **Assignment 4 — Security Middleware, Validation, and Password Hashing** +# Week 4 Assignment: Protected Tasks, Validation, and Password Hashing -## **Assignment Instructions** +## Learning Objectives -All of the work for this assignment goes into your project. You will not use the assignment4 folder. Instead, you'll make changes to your app.js and to your controllers, routers, and middleware. Before you start, create a new branch called `assignment4` from the main branch. +- Add task create, read, update, and delete routes to the main todo backend +- Protect task routes with authentication middleware +- Check that users can only access their own tasks +- Validate user and task request bodies with Joi +- Store password hashes instead of plain-text passwords -### **The Task Routes** +## Assignment Guidelines -You have created route handlers that allow users to register, log in, and log off. Now, you will add capabilities so that each user can create, update, modify, and delete on task entries. Here is the specification for your work. But don't start yet. +NOTE: The AI review tool (known as AirHub) can check code and structure, but it does not run your code in a server environment to verify that aspect runs properly. We will have human reviewers checking this aspect, so you may receive a passing assignment from AirHub that could still need revisions after a human has checked that your work runs properly in the correct environment. If your AI and human reviewer feedbacks don't match, trust the human review. -Create a task controller and a task router. You need to support the following routes: +1. **Setup** + - Work in your `node-homework` repository. + - All Assignment 4 work goes into the main todo backend. + - Do not create or use an `assignment4` folder. +2. **Create a branch** + - Create a new branch for your work on assignment 4, such as `assignment4`. + - Make your changes and commits on that branch. +3. **Before you test** + - Please read the TDD Testing Guide for how to run and interpret the course-provided tests: [TDD Testing Guide](?page=test-driven-development-(tdd)-testing-guide) +4. **Run the tests** + - Run the required Assignment 4 test with: + ```bash + npm run tdd assignment4a + ``` + - If you attempt the optional advanced section, run: + ```bash + npm run tdd assignment4b + ``` -1. POST "/api/tasks" (the `create` function) This creates a new entry in the list of tasks for the currently logged on user. +## What You Are Building -2. GET "/api/tasks" (`index`). This returns the list of tasks for the currently logged on user. +In Assignment 3, you added user registration, logon, and logoff to the main todo backend. -3. GET "/api/tasks/:id" (`show`). This returns the task with a particular ID for the currently logged on user. +In Assignment 4, you will add: -4. PATCH "/api/tasks/:id" (`update`). This updates the task with a particular ID for the currently logged on user. +- Task routes +- Task controller functions +- Authentication middleware for task routes +- Joi validation schemas +- Password hashing -5. DELETE "/api/tasks/:id (`deleteTask`)". This deletes the task with a particular ID for the currently logged on user. +You are still using temporary in-memory storage: `global.users`, `global.tasks`, and `global.user_id`. -So, that's five functions you need in the task controller, and five routes that you need in the task router. But, we have a few problems: +This is still a scaffold. Later, you will replace this temporary storage with PostgreSQL and Prisma. -- What if there is no currently logged on user? -- How do you assign an ID for each task? -- To get, patch, or delete a task, -- how do you figure out which one you are going to work on? +## Assignment Parts and Test Files -Let's solve each of these. First, for every task route, we need to check whether there is a currently logged on user, and to return a 401 if there isn't. If there is a logged on user, the job should pass to the task controller, and the task controller should handle the request. So -- that's middleware. Create a `/middleware/auth.js` file. In it, you need a single function. The function doesn't have to have a name, because it's going to be the only export. It checks: is there a logged on user, that is, is `global.user_id` null? If it is null, it returns an UNAUTHORIZED status code and a JSON message that says "unauthorized". If there is a logged on user, it calls next(). That sends the request on to the tasks controller. Be careful that you don't do both of these: res.json() combined with next() would mess things up. +This assignment has one required core part and one optional advanced check: -In app.js, you can then do: +1. **Assignment 4A - Protected Tasks, Validation, and Password Hashing** + - This is part of the main app you will keep building throughout the course. + - It covers the required task routes, auth middleware, task ownership, basic validation, and password hashing behavior. + - Test command: + ```bash + npm run tdd assignment4a + ``` -```js -const authMiddleware = require("./middleware/auth"); +2. **Assignment 4B - Advanced Validation, Patch Updates, and Password Security** (optional) + - This checks deeper validation, patch update, and password security behavior. + - It uses the same main todo backend files. + - Test command: + ```bash + npm run tdd assignment4b + ``` + +The core tasks below are required. The advanced section at the end gives extra context and uses the optional `assignment4b` test. + +## Core Tasks (Required) + +### 1. Install Joi + +Install Joi in your `node-homework` project: + +```bash +npm install joi ``` -But, `app.use(authMiddleware)` would protect any route. Then no one could register or log on. You want it only in front of the tasks routes. So, you do the following: +Joi is the validation library you will use for user and task request bodies. -```js -const taskRouter = require("./routers/taskRoutes"); -app.use("/api/tasks", authMiddleware, taskRouter); +### 2. Add the New Files + +Create these files if they do not already exist: + +```text +controllers/taskController.js +routes/taskRoutes.js +middleware/auth.js +validation/userSchema.js +validation/taskSchema.js ``` -That solves the first problem. The authMiddleware gets called before any of the task routes, and it makes sure that no one can get to those routes without being logged on. These are called "protected routes" because they require authentication. +The tests call some controller functions directly, so your controller functions need to work even when the request does not go through the full Express app. + +### 3. Add Authentication Middleware -Protected routes act as a security barrier - they check if a user has a valid session before allowing access to sensitive operations like creating, reading, updating, or deleting tasks. Without this protection, anyone could potentially access or modify other users' data, which would be a serious security vulnerability in a real application. +Create `middleware/auth.js`. -Let's go on to problem 2. +This file is the gatekeeper for task routes. The task controllers should only run after the app knows someone is logged in. -Create a file called `controllers/taskController.js`. You need the following request handler functions within it: +This middleware should: -- create -- index -- show -- update -- deleteTask +- Check whether `global.user_id` exists +- Return status `401` if no user is logged in +- Return JSON with a message such as `"Unauthorized"` +- Call `next()` if a user is logged in -Each task should have a unique ID. So, create a little counter function in taskController.js, as follows: +Example: ```js -const taskCounter = (() => { - let lastTaskNumber = 0; - return () => { - lastTaskNumber += 1; - return lastTaskNumber; - }; -})(); +if (!global.user_id) return res.status(401).json({ message: "Unauthorized" }); ``` -This is a closure. You are sometimes asked to write a closure in job interviews. We can use this to generate a unique ID for each task -- but restart the server and you start over. +Do not call `next()` after sending the `401` response. -Each of the task objects needs a userId attribute, which records who owns that task. For the time being, you'll put the user's email in that attribute. +### 4. Add Task Routes -In taskController.js, you need a function called `create(req, res)`. And inside that, you do: +Create `routes/taskRoutes.js`. -```js -const newTask = {...req.body, id: taskCounter(), userId: global.user_id.email}; -global.tasks.push(newTask); -const {userId, ...sanitizedTask} = newTask; -// we don't send back the userId! This statement removes it. -res.json(sanitizedTask); +This router keeps the task URLs separate from the controller logic. The router decides which controller function should run for each HTTP method and path. + +It should connect these routes to task controller functions: + +```text +POST /api/tasks -> create +GET /api/tasks -> index +GET /api/tasks/:id -> show +PATCH /api/tasks/:id -> update +DELETE /api/tasks/:id -> deleteTask ``` -In this REST call, as in all subsquent ones, if the operation succeeds, you return the corresponding result code and the new or updated or deleted object. The successful result code is typically 200, meaning OK, except for creates, when it is 201. +The tests require these exact controller function names. -Now for problem 3. When you have a route defined with a colon `:`, that has a special meaning. The string following the colon is the name of a variable, and when a request comes in for this route, Express parses the value of the variable and stores it in req.params. For the routes above, you would have `req.params.id`. Now, be careful: this is a string, not an integer, so you need to convert it to an integer before you go looking for the right task. Also, the string that is passed might not be a valid id, which should be a number. If not, you need to return an error. +Inside `routes/taskRoutes.js`, the paths are relative to `/api/tasks`: -The other thing to be careful about is access control. The only tasks that the currently logged on user should be able to delete are their own! You have to check that the `task.userId` contains the right email. +```text +GET / -> index +GET /:id -> show +POST / -> create +PATCH /:id -> update +DELETE /:id -> deleteTask +``` + +### 5. Protect the Task Routes in `app.js` -Here's how you could do it in a deleteTask(req,res) function in your task controller: +In `app.js`, require the auth middleware and task router: ```js -const taskToFind = parseInt(req.params?.id); // if there are no params, the ? makes sure that you - // get a null -if (!taskToFind) { - return res.status(400).json(message: "The task ID passed is not valid.") -} -const taskIndex = global.tasks.findIndex((task) => task.id === taskToFind && task.userId === global.user_id.email); -// we get the index, not the task, so that we can splice it out -if (taskIndex === -1) { // if no such task - return res.status(StatusCodes.NOT_FOUND).json({message: "That task was not found"}); - // else it's a 404. -} -const { userId, ...task } = global.tasks[taskIndex]; -// pull userId out and keep a copy of everything else, so the response is sanitized -global.tasks.splice(taskIndex, 1); // do the delete -return res.json(task); // return the deleted entry without its userId. The default status code, OK, is returned +const authMiddleware = require("./middleware/auth"); ``` -Now, write the remaining methods. +```js +const taskRouter = require("./routes/taskRoutes"); +``` -**Hint 1** The task objects you send back should not include a userId. Consider the case for the index operation. You can get a list of tasks as follows: +Mount the task router with auth middleware: ```js - const userTasks = global.tasks.filter((task) => task.userId === global.user_id.email); +app.use("/api/tasks", authMiddleware, taskRouter); ``` -Ok, so far so good. But you don't send the userId values back. And you can't mutate the tasks in this list, because that would update them in place, and then those entries in `global.tasks` wouldn't have userId attributes. So you need to make a copy of each, and take the userId out of that copy, as follows: +This protects task routes only. + +Do not put `authMiddleware` in front of the user routes. Users need to register and log on before they can access protected routes. + +This is the difference between public and protected routes. User routes stay public so a user can start a session. Task routes are protected because they work with private user data. + +### 6. Create the Task ID Counter + +In `controllers/taskController.js`, add a small `taskCounter()` helper that returns the next task ID each time it is called. + +This is temporary. Later, the database will create task IDs. + +You need task IDs now because `show`, `update`, and `deleteTask` all need a way to identify one specific task. + +### 7. Add Task Ownership + +Each task should store the email of the user who owns it. + +Authentication tells the app who is logged in. Ownership tells the app which tasks that user is allowed to access. + +For now, use: ```js -const sanitizedTasks = userTasks.map((task) => { - const { userId, ...sanitizedTask} = task; - return sanitizedTask; -}); +userId: global.user_id.email ``` -In the above, you are copying everything except the userId to the new sanitizedTask, and then returning an array of those. - -**Hint 2** When you do the update, you **DO** want to mutate the task object in place. You are doing a patch. You don't want a complete replacement of the task object. You use all the values from the body, but you leave any attributes of the task that aren't in the new body unchanged. There is a concise way to do this (after you find the right task object to mutate). +Task objects stored in `global.tasks` should have this shape: ```js -Object.assign(currentTask, req.body) +{ + id: 1, + title: "first task", + isCompleted: false, + userId: "jim@sample.com" +} ``` -This pattern is worth remembering. But the database will handle this automatically for you when you call an update. After you mutate the task as above, you **still** have to make a copy that doesn't include the userId, and send that back. +Do not include `userId` in task responses. -### **Postman Testing** +The `userId` field is internal bookkeeping. The server needs it for authorization checks, but the client does not need to receive it. -You next create Postman tests for all fo the task operations above. You want to check: +Use this pattern to remove it from a response copy: -- If no one is logged on, a 401 is returned for these operations. -- If a user is logged on, all CRUD operations can be performed for tasks belonging to that user. -- If user 1 owns a task, user 2 can't do any CRUD operations on that task. +```js +const { userId, ...sanitizedTask } = task; +``` + +### 8. Create `validation/userSchema.js` -To do the last test, you logon as user 1, create some tasks, write down the id's of each, log on as user 2, and verify that every CRUD attempt returns a 404. +Create a `validation` folder if it does not already exist. -### **The Automated Tests** +In `validation/userSchema.js`, create and export `userSchema`. -Run `npm run tdd assignment4` to see if your code works as expected. Not all the tests will pass, because you haven't completed the assignment yet. +This schema describes what a valid user registration body looks like. Without it, the app could store missing emails, invalid emails, very short names, or weak passwords. -### **Validation of User Input** +Rules: -At present, your app your app accepts any data sent to it with Postman. There is no validation whatsoever. Let's fix that. There are various ways to validate user data. We will eventually use a database access tool called Prisma, which has built-in runtime validation but is very TypeScript-oriented. So we'll use a different library called Joi. Install it now using `npm install joi` command. +- `email` is required, trimmed, lowercased, and must be valid email format. +- `name` is required, trimmed, and must be 3 to 30 characters. +- `password` is required and must not be trivial. -Consider a user entry. You need a name, an email, and a password. You don't want any leading or trailing spaces. You can't check whether the email is a real one, but you can check if it complies with the standards for email addresses. You want to store the email address in lowercase, because you need it to be unique in your data store, so you don't want to deal with case variations. You don't want trivial, easily guessed passwords. All of these attributes are required. +Useful Joi patterns: -Consider a task entry. You need a title. You need a boolean for `isCompleted`. If that is not provided, you want it to default to false. The title is required in your `req.body` when you create the task entry, but if you are just updating the isCompleted, the patch request does not have to have a title. We won't worry about the task id -- you automatically create this in your app. In the database, each task will also have a userId, indicating which user owns the task, but that will be automatically created too. +```js +email: Joi.string().trim().lowercase().email().required() +``` -Joi provides a very simple language to express these requirements. The Joi reference is [here](https://joi.dev/api/?v=17.13.3). If a user sends a request where the data doesn't meet the requirements, Joi can provide error messages to send back. And, if the entry to be created needs small changes, like converting emails to lower case, or stripping off leading and trailing blanks, Joi can do that too. +```js +name: Joi.string().trim().min(3).max(30).required() +``` -Create a folder called validation. Create two files in that folder, userSchema.js and taskSchema.js. Here's the code for userSchema.js: +Your file will have this general shape: ```js const Joi = require("joi"); const userSchema = Joi.object({ - email: Joi.string().trim().lowercase().email().required(), - name: Joi.string().trim().min(3).max(30).required(), - password: Joi.string() - .trim() - .min(8) - .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z0-9]).+$/) - .required() - .messages({ - "string.pattern.base": - "Password must be at least 8 characters long and include upper and lower case letters, a number, and a special character.", - }), + email: ..., + name: ..., + password: ..., }); module.exports = { userSchema }; ``` -You can look at the code and to understand what it does. There are some good convenience functions, such as `.email()`, which checks for a syntactically valid email. The only complicated one is the password. This is a simple check for trivial passwords. The password pattern uses a regular expression, and the customized error message explains what is wrong if the password doesn’t meet requirements. +### 9. Create `validation/taskSchema.js` + +In `validation/taskSchema.js`, create and export `taskSchema` and `patchTaskSchema`. + +Task creation and task updates need different rules. Creating a task needs a title. Updating a task can change only one field, but it should not accept an empty body. + +Rules: + +- Creating a task requires `title`. +- Creating a task defaults `isCompleted` to `false` if it is not provided. +- Updating a task allows partial updates. +- Updating a task must include at least one field. + +Useful Joi patterns: + +```js +title: Joi.string().trim().min(3).max(30).required() +``` + +```js +isCompleted: Joi.boolean().default(false).not(null) +``` -Here is the code for taskSchema.js: +```js +Joi.object({ ... }).min(1) +``` + +Your file will have this general shape: ```js const Joi = require("joi"); const taskSchema = Joi.object({ - title: Joi.string().trim().min(3).max(30).required(), - isCompleted: Joi.boolean().default(false).not(null), + title: ..., + isCompleted: ..., }); const patchTaskSchema = Joi.object({ - title: Joi.string().trim().min(3).max(30).not(null), - isCompleted: Joi.boolean().not(null), -}).min(1).message("No attributes to change were specified."); + title: ..., + isCompleted: ..., +}).min(1); module.exports = { taskSchema, patchTaskSchema }; ``` -The `min(1)` means that while both `title` and `isCompleted` are optional in a patch task request, you have to have one of those attributes -- otherwise there's nothing to do. To do a validation, you do the following: +### 10. Validate User Registration + +Update `controllers/userController.js`. + +Validation should happen before the controller checks for duplicate users or stores anything. If the request body is not valid, the controller should stop early with a `400` response. + +Import the user schema: ```js -const {error, value} = userSchema.validate({name: "Bob", email: "nonsense", password: "password", favoriteColor: "blue"}, {abortEarly: false}) +const { userSchema } = require("../validation/userSchema"); ``` -You do `{abortEarly: false}` so that you can get all the error information to report to the user, not just the first failure. When the validate() call returns, if error is not null, there is something wrong with the request, and `error.message` says what the error is. If error is null, then value has the object you want to store, which may be different from the original. The email would have been converted to lower case, for example. In this case, the email is invalid, the password fails the pattern, and favoriteColor is not part of the schema, so there are three errors. - -Add validations to your create operations for users and tasks, and your to your update operation for tasks. It is possible that these requests might be sent without a body, so you must first have: +In `register`, validate `req.body` before creating a user: ```js -if (!req.body) req.body = {}; +const { error, value } = userSchema.validate(req.body, { abortEarly: false }); ``` -Otherwise, validation won't work right. You then validate `req.body`. If you get an error, you return a BAD_REQUEST status, and you send back a JSON body with the error message provided by the validation. If you don't get an error, you go ahead and store the returned value, returning a CREATED, or an OK if an update completes. Then test your work with Postman, trying both good and bad requests. +Use `value`, not raw `req.body`, when creating the user. Joi may trim or lowercase the data. -### **Storing Only a Hash of the Passwords** +If validation fails, return status `400` with the validation message. -You should never store user passwords. If your database were ever compromised, your users would face major security vulnerabilities, especially because many people reuse passwords. +The validation belongs near the top of `register`, before checking for an existing user and before creating the new user: -Instead, at user registration, you create a random salt, concatenate the password and the salt, and compute a cryptographically secure hash. You store the hash plus the salt. Each user's password has a different salt. When the user logs on, you get the salt back out, concatenate the password the user provides with the salt, hash that, and compare that with what you've stored. You need a cryptography routine to do the hashing. The scrypt algorithm is a good one. Although bcrypt is still common, it has known weaknesses and is considered now outdated. Scrypt is the old callback style, so you use util.promisify to convert it to promises. Add the following code to userController.js: +```js +exports.register = async (req, res) => { + if (!req.body) req.body = {}; + const { error, value } = userSchema.validate(...); + if (error) return res.status(400).json(...); + + // use value.email, value.name, and value.password after this point + ... +}; +``` + +### 11. Add Password Hashing + +In `controllers/userController.js`, add these imports and helper setup: ```js const crypto = require("crypto"); const util = require("util"); const scrypt = util.promisify(crypto.scrypt); +``` -async function hashPassword(password) { - const salt = crypto.randomBytes(16).toString("hex"); - const derivedKey = await scrypt(password, salt, 64); - return `${salt}:${derivedKey.toString("hex")}`; -} +`crypto.scrypt` uses callback style. `util.promisify()` lets you use it with `async` and `await`. -async function comparePassword(inputPassword, storedHash) { - const [salt, key] = storedHash.split(":"); - const keyBuffer = Buffer.from(key, "hex"); - const derivedKey = await scrypt(inputPassword, salt, 64); - return crypto.timingSafeEqual(keyBuffer, derivedKey); -} +Add these helper functions: + +- `hashPassword(password)`: creates a salt, hashes the password, and returns the stored hash string. +- `comparePassword(inputPassword, storedHash)`: hashes the submitted password with the stored salt and compares the result. + +Useful crypto patterns: + +```js +const salt = crypto.randomBytes(16).toString("hex"); +``` + +```js +return `${salt}:${derivedKey.toString("hex")}`; +``` + +In `register`: + +- Validate the request body +- Use the validated `value` +- Hash the password +- Store `hashedPassword` +- Do not store plain `password` + +The goal is that `global.users` never stores the original password. It should store enough information to check a future login, but not the password itself. + +Example structure: + +```js +const newUser = { email, name, hashedPassword }; +``` + +The hashing belongs after validation succeeds and before you push the user into `global.users`: + +```js +const hashedPassword = await hashPassword(value.password); +const newUser = { email: value.email, name: value.name, hashedPassword }; +``` + +In `logon`: + +- Find the user by email +- Compare the submitted password with `user.hashedPassword` +- Return `401` if the credentials do not match + +After this change, logging in should no longer compare `password` to `user.password`. The stored user should have `hashedPassword` instead. + +The password comparison happens after you find the user: + +```js +const goodCredentials = user && await comparePassword(password, user.hashedPassword); +``` + +### 12. Implement `create` in `taskController.js` + +Import `taskSchema`: + +```js +const { taskSchema, patchTaskSchema } = require("../validation/taskSchema"); +``` + +The `create` function should: + +- Validate `req.body` with `taskSchema` +- Return `400` if validation fails +- Create a new task with `id`, `userId`, and the validated values +- Push the task into `global.tasks` +- Return status `201` +- Return the task without `userId` + +Use the validated task data to build the stored task. This matters because Joi may add `isCompleted: false` when the request did not include it. + +Example structure: + +```js +const newTask = { id: taskCounter(), userId: global.user_id.email, ...value }; +``` + +The validation belongs at the start of `create`, before creating the task: + +```js +exports.create = async (req, res) => { + if (!req.body) req.body = {}; + const { error, value } = taskSchema.validate(...); + if (error) return res.status(400).json(...); + + // build and store the task after validation passes + ... +}; +``` + +### 13. Implement `index` + +The `index` function should: + +- Get tasks for the logged-in user +- Return `404` if the user has no tasks +- Return status `200` and an array of tasks if tasks exist +- Remove `userId` from every returned task + +This function should not return every task in `global.tasks`. It should return only the tasks owned by the logged-in user. + +Example filtering: + +```js +const userTasks = global.tasks.filter( + (task) => task.userId === global.user_id.email, +); +``` + +Example sanitizing: + +```js +const { userId, ...sanitizedTask } = task; +``` + +For an array of tasks, use the same sanitizing pattern inside `.map()`. + +### 14. Implement `show` + +The `show` function should: + +- Convert `req.params.id` to a number +- Return `400` if the ID is not valid +- Find a task with that ID and the logged-in user's email +- Return `404` if no matching task exists +- Return status `200` and the task without `userId` + +Finding by ID is not enough. Two users should not be able to see each other's tasks, so the lookup also needs the current user's email. + +Example: + +```js +const taskId = parseInt(req.params?.id); +``` + +After parsing the ID, search for a task that matches both `taskId` and `global.user_id.email`. + +### 15. Implement `update` + +The `update` function should: + +- Validate `req.body` with `patchTaskSchema` +- Return `400` if validation fails +- Convert `req.params.id` to a number +- Find a task with that ID and the logged-in user's email +- Return `404` if no matching task exists +- Merge the validated patch fields into the stored task +- Return status `200` and the updated task without `userId` + +Validate first, then find the task, then update it. That order keeps invalid data from being written into the in-memory task list. + +Use this pattern: + +```js +Object.assign(task, value); +``` + +This copies the validated patch fields onto the existing task. + +The update flow should be: + +```js +validate patch body -> parse id -> find owned task -> Object.assign(...) -> return sanitized task +``` + +### 16. Implement `deleteTask` + +The `deleteTask` function should: + +- Convert `req.params.id` to a number +- Return `400` if the ID is not valid +- Find the task index for that ID and the logged-in user's email +- Return `404` if no matching task exists +- Remove the task from `global.tasks` +- Return status `200` and the deleted task without `userId` + +Use `findIndex()` for delete because you need the array position in order to remove the task with `splice()`. + +When finding the task index, check both the task ID and `global.user_id.email`. + +The delete flow should be: + +```js +parse id -> find owned task index -> copy task without userId -> splice -> return copied task ``` -This code implements the hashing described. You can stare at it a bit, but your typical AI helper can generate this code any time. There's not much to learn or remember. +### 17. Test With Postman + +Test these cases manually: + +- No logged-in user gets `401` for task routes +- A logged-in user can create, list, show, update, and delete their own tasks +- A second user cannot access the first user's tasks +- Invalid task IDs return `400` +- Missing tasks return `404` +- Invalid user and task bodies return `400` + +### 18. Run the Automated Tests -Change the register function to call hashPassword. Right now, a user entry looks like `{ name, email, password }`. Instead, store `{name, email, hashedPassword }`. Also, change the login method to use comparePassword. Note that these are async functions, so you have to await the result. Once you have done all of this, test with Postman. Then run the automated tests with +Run: ```bash -npm run tdd assignment4 +npm run tdd assignment4a ``` -It's good that you got this fixed while you were storing passwords only in memory. The next step for your project application is to store user and task records in a database. +The basic tests check: -## Video Submission +- User register/logon/logoff still work +- Task controller functions work +- Users cannot access each other's tasks +- Task responses do not include `userId` +- Basic validation and password hashing are in place -Record a short video (3–5 minutes) on YouTube, Loom, or similar platform. Share the link in your submission form. +If you attempt the optional advanced section, also run: -**Video Content**: Answer 3 questions from Lesson 4: +```bash +npm run tdd assignment4b +``` -1. **How do you protect routes using middleware in Express?** - - Explain the concept of protected routes and why they're important - - Show how to create authentication middleware - - Demonstrate how to apply middleware to specific routes - - Discuss the difference between protected and public routes +The optional advanced tests check deeper validation, patch update, and password security behavior: + +- User and task schemas exist +- Validation rejects invalid user/task data +- Patch updates merge fields without replacing the stored task +- Passwords are stored as hashes instead of plain text + +## Suggested File Structure + +By the end of Assignment 4, your main app files should include: + +```text +node-homework/ + app.js + controllers/ + userController.js + taskController.js + routes/ + userRoutes.js + taskRoutes.js + middleware/ + auth.js + not-found.js + error-handler.js + validation/ + userSchema.js + taskSchema.js +``` -2. **What security vulnerabilities does data validation prevent and how do you implement validation?** - - Explain how validation prevents attacks like SQL injection and XSS - - Show your userSchema.js and taskSchema.js files and explain each validation rule +## Advanced Knowledge (Optional) -3. **Why should you never store passwords in plain text and what are the security principles for password hashing?** - - Explain the security risks of storing plain text passwords - - Explain why you need salt and what rainbow attacks are - - Discuss why you should never invent your own cryptography - - Explain the difference between hashing and encryption +The following ideas give more context. They are useful, but the core tasks above are what you need to complete the assignment. -**Video Requirements**: -- Keep it concise (3-5 minutes) -- Use screen sharing to show code examples (when needed) -- Speak clearly and explain concepts thoroughly -- Include the video link in your assignment submission +If you work through this optional advanced section, use `npm run tdd assignment4b` to check the deeper validation, patch update, and password security cases. + +### `Object.assign()` and Patch Updates + +PATCH means partial update. + +If the request body is: + +```js +{ + isCompleted: true +} +``` + +You do not want to replace the whole task with only that object. You only want to update the fields that were sent. + +`Object.assign(task, value)` mutates the existing task object stored in `global.tasks`. + +After using it, still remove `userId` from the response. -## **Submit Your Assignment on GitHub** +### Password Security Details -📌 **Follow these steps to submit your work:** +The hashing helpers store a value in this format: -#### **1️⃣ Add, Commit, and Push Your Changes** +```text +salt:hash +``` + +Each password gets a different salt. This helps protect against precomputed password attacks. + +`crypto.timingSafeEqual()` compares values in a safer way than a simple string comparison. + +You do not need to memorize these internals. The important rule is to use trusted crypto tools and never invent your own password storage system. + +The same idea applies to other sensitive data. Do not store credit card numbers, government ID numbers, or other private information unless the app truly needs it and you understand the legal and security requirements. + +### Validation Boundaries + +Joi validates request data before your app stores it. + +Joi does not replace authorization. A task body can be valid and still belong to another user. + +Later, database constraints will add another layer of protection. + +### Status Code Nuance + +For this assignment: + +- Use `401` when no user is logged in. +- Use `400` when request data is invalid. +- Use `404` when the task is not found for this user. + +Some APIs use `403` when a logged-in user is not allowed to access a resource. This assignment can use `404` for another user's task so the API does not reveal whether that task exists. -- Within your node-homework folder, do a git add and a git commit for the files you have created, so that they are added to the `assignment4` branch. -- Push that branch to GitHub. +### Future Authentication Direction -#### **2️⃣ Create a Pull Request** +`global.user_id` is only a learning scaffold. -- Log on to your GitHub account. -- Open your `node-homework` repository. -- Select your `assignment4` branch. It should be one or several commits ahead of your main branch. -- Create a pull request. +Later, the app should know which client made the request. Common production patterns include sessions, cookies, and tokens. -#### **3️⃣ Submit Your GitHub Link** +You do not need to implement those in Assignment 4. The important idea is that the current global login will be replaced later. -- Your browser now has the link to your pull request. Copy that link. -- Paste the URL into the **assignment submission form**. -- **Don't forget to include your video link in the submission form!** +## Video Submission + +Record a short video (3-5 minutes) on YouTube, Loom, or similar platform. Share the link in your submission form. + +**Video Content**: Answer 3 questions from the core Lesson 4 material: + +1. **What is the difference between authentication and authorization?** + - Explain what `global.user_id` represents in this assignment + - Explain how task ownership is checked + +2. **How does Joi validation fit into user and task creation?** + - Explain what `error` and `value` mean + - Explain why controllers should use validated `value` + +3. **Why should passwords be hashed before they are stored?** + - Explain why plain-text passwords are dangerous + - Explain what changes in `register` and `logon` + +**Video Requirements**: + +- Keep it concise (3-5 minutes) +- Use screen sharing to show code examples when useful +- Speak clearly and explain concepts thoroughly +- Include the video link in your assignment submission +## To Submit an Assignment +1. Do these commands: + ```bash + git add -A + git commit -m "some meaningful commit message" + git push origin assignment4 + ``` +2. Go to your `node-homework` repository on GitHub. +3. Select your `assignment4` branch. +4. Create a pull request. The target of the pull request should be the main branch of your GitHub repository. +5. Once the pull request is created, your browser contains the URL of the PR. Include that link in your homework submission. +6. Do not forget to include your video link in the submission form. diff --git a/lessons/04-tasks-validations.md b/lessons/04-tasks-validations.md index 0064ce8..6f666ff 100644 --- a/lessons/04-tasks-validations.md +++ b/lessons/04-tasks-validations.md @@ -1,64 +1,627 @@ -# **Lesson 4 — Security Middleware, Validation, and Password Hashing** +# **Lesson 4 — Protected Routes, Validation, and Password Hashing** ## **Lesson Overview** -**Learning objective**: Students will learn how to protect routes using middleware. Students will learn about the importance of validation of data before it is stored or updated, and how to do this validation. Students will learn that only a cryptographic hash of a password should be stored, and best practices on how to do that. +**Learning objective**: You will learn how to protect task routes, check whether a logged-in user owns a task, validate incoming data with Joi, and store password hashes instead of plain-text passwords. + +This lesson is split into two parts: + +- **Part 1 — Core Knowledge**: required material you should know to continue the course. +- **Part 2 — Advanced Knowledge**: optional context and reference material. **Topics**: -1. Protected Routes and Middleware -2. Data Validation for Create and Update Operations -3. Hashing Passwords +1. What Week 4 adds to the todo backend +2. Authentication and authorization +3. Limits of the temporary global login +4. Protected task routes +5. Task ownership +6. Task controller functions +7. Joi validation +8. Password hashing with Node `crypto` +9. Patch updates with `Object.assign()` +10. Validation and security boundaries + +--- + +# **Part 1 — Core Knowledge** + +## **4.1 What Week 4 Adds** + +In Week 3, you organized the main todo backend with controllers, routes, middleware, and basic error handling. + +In Week 4, you keep building that same app. + +You will add: + +- Task create, read, update, and delete routes +- Protected task routes +- Ownership checks +- Joi validation +- Password hashing + +The main app is still using temporary in-memory data: + +```js +global.users = []; +global.tasks = []; +global.user_id = null; +``` + +That temporary setup lets you practice the backend patterns before the app moves to PostgreSQL and Prisma later. + +## **4.2 Authentication and Authorization** + +Authentication and authorization are related, but they are not the same. + +**Authentication** asks: + +```text +Who is logged in? +``` + +**Authorization** asks: + +```text +Is this logged-in user allowed to access this resource? +``` + +In this assignment, `global.user_id` is the temporary authentication signal. If it has a user object, someone is logged in. If it is `null`, no one is logged in. + +Task ownership is the authorization check. A logged-in user should only be able to see, update, or delete their own tasks. + +For now, each task stores the owner's email: + +```js +{ + id: 1, + title: "Finish homework", + isCompleted: false, + userId: "jim@sample.com" +} +``` + +When a task controller looks up a task, it should check both the task ID and the current user's email. + +```js +const task = global.tasks.find( + (task) => task.id === taskId && task.userId === global.user_id.email, +); +``` + +That check answers: "Does this task belong to the logged-in user?" + +## **4.3 Limits of `global.user_id`** + +The `global.user_id` approach is a scaffold. It is useful for learning, but it is not production-ready. + +Important limits: + +- It is shared by the whole server. +- It only represents one logged-in user at a time. +- It resets when the server restarts. +- It does not prove which browser or client made the request. + +You may hear this idea described as a `loggedOnUser` variable in general examples. In this course project, the actual variable name is `global.user_id`. + +Later, you will replace this temporary approach with stronger authentication patterns. + +## **4.4 Protected Task Routes** + +Some routes should be public. Users need to register and log on before they can be protected. + +Public routes: + +```text +POST /api/users/register +POST /api/users/logon +POST /api/users/logoff +``` + +Task routes should be protected: + +```text +POST /api/tasks +GET /api/tasks +GET /api/tasks/:id +PATCH /api/tasks/:id +DELETE /api/tasks/:id +``` + +A protected route checks whether a user is logged in before the controller runs. + +You already learned middleware in Week 3. Here, you are applying that same middleware pattern to authentication. + +Create `middleware/auth.js`: + +```js +module.exports = (req, res, next) => { + if (!global.user_id) { + return res.status(401).json({ + message: "Unauthorized", + }); + } + + next(); +}; +``` + +The `return` matters. If the middleware sends a `401` response, it should not also continue to the controller. + +In `app.js`, apply this middleware only to task routes: + +```js +const authMiddleware = require("./middleware/auth"); +const taskRouter = require("./routes/taskRoutes"); + +app.use("/api/tasks", authMiddleware, taskRouter); +``` + +Do not put this middleware in front of every route. If you did, users would need to be logged in before they could register or log on. + +## **4.5 Task Ownership** + +Each task should store who owns it. + +For this assignment, use the logged-in user's email: + +```js +const newTask = { + id: taskCounter(), + userId: global.user_id.email, + ...value, +}; +``` + +Do not send `userId` back in API responses. + +The client needs task information like: + +```json +{ + "id": 1, + "title": "Finish homework", + "isCompleted": false +} +``` + +The client does not need the internal owner field: + +```json +{ + "userId": "jim@sample.com" +} +``` + +You can remove `userId` by copying the task and leaving that property out: + +```js +const { userId, ...sanitizedTask } = task; +res.status(200).json(sanitizedTask); +``` + +This does not delete `userId` from the stored task. It only leaves `userId` out of the response copy. + +## **4.6 Task Controller Functions** + +Create `controllers/taskController.js`. + +Export these five functions: + +```js +create +index +show +update +deleteTask +``` + +### **Task IDs** + +For now, use a small counter helper: + +```js +const taskCounter = (() => { + let lastTaskNumber = 0; + return () => { + lastTaskNumber += 1; + return lastTaskNumber; + }; +})(); +``` + +Each time you call `taskCounter()`, it returns the next number. + +This is still temporary. When you use a database later, the database will create IDs for you. + +### **Route Parameters** + +For routes like this: + +```text +GET /api/tasks/:id +``` + +Express stores the value in `req.params.id`. + +That value is a string, so convert it before comparing it to your numeric task IDs: + +```js +const taskId = parseInt(req.params?.id); + +if (!taskId) { + return res.status(400).json({ + message: "The task ID passed is not valid.", + }); +} +``` + +### **Create** + +`create` should: + +- Validate the request body +- Create a task with an ID +- Store the current user's email in `userId` +- Push the task into `global.tasks` +- Return status `201` +- Return the task without `userId` + +### **Index** + +`index` should: + +- Find tasks owned by the logged-in user +- Return those tasks without `userId` +- Return `404` if this user has no tasks + +### **Show** -## **4.1 Protected Routes and Middleware** +`show` should: -There aren't a lot of concepts to cover in this week's lesson. They are important topics, but we'll cover them quickly. On the other hand, the assignment is quite a bit of work. The assignment includes implementation of the concepts of the lesson. +- Read `req.params.id` +- Find a task with that ID and the current user's email +- Return the task without `userId` +- Return `404` if no matching task exists -You do not want to give unauthenticated users access to private or sensitive data. You do not want to give authenticated users access to data that they are not authorized to access. The task information your application will store and retrieve is a case in point. An unauthenticated user should not be able to access any task records. An authenticated user should not be able to access any task records except their own. +### **Update** -You are about to create route handlers in the task controller. Each of these route handlers could check, on every operation, if the user has been authenticated and if the requested tasks actually belong to that user. But, that would violate the DRY (Don't Repeat Yourself) principle. You want one place in the code that establishes if the user has been authenticated and who that user is, before any of the route handlers for tasks are invoked. For that, we use a middleware function. This middleware checks to see if a user is logged on, and if not, it returns a 401 (unauthorized). If a user is logged on, the middleware typically stores information about the user identity in req.user, so that route handlers can use that to enforce authorization policies. For the moment, we are storing the information about the logged on user in a global variable, which is loggedOnUser. However, that's a temporary makeshift. It means that only one user can be logged on at a given time, and if that user is logged on, anyone can access protected data as if they were that user. These are serious defects, and we'll fix them in a later lesson. +`update` should: -For your assignment, you'll create a middleware function that just checks if someone is logged on, and returns the 401 if no one is. You'll then put that middleware function in front of all the task routes. You do not put this middleware function in front of all of your routes, because then someone would have to be logged on in order to do a logon. +- Validate the patch body +- Read `req.params.id` +- Find a task with that ID and the current user's email +- Merge the validated patch fields into the stored task +- Return the updated task without `userId` -## **4.2 Data Validation for Create and Update Operations** +Use this pattern to merge patch fields: -Whenever data is stored or changed, your API should verify the new or updated records are valid. Currently, in your userController, anyone could store arbitrary user data. When you change the storage mechanism from the current in memory store to a database, you'll configure the database with some validation, but databases only perform limited checks. Sometimes validation needs to be done for delete operations too. You would not want a record describing a customer to be deleted, if there are orders for that customer in the database. +```js +Object.assign(task, value); +``` -Eventually (Lesson 6) you'll use a package called Prisma for data access. Prisma can do data validation, so that it occurs automatically with the data access. However, validation in Prisma is configured using TypeScript. This class does not use TypeScript, so we'll use another data validation tool called Joi. Some amount of data transformation may occur at the validation step, such as converting emails to lower case and trimming leading and trailing blanks. Joi does that as well. +For now, the important idea is that this copies the validated patch fields onto the existing task. - Joi acts as your first line of defense by filtering out malicious data before it reaches your application. It prevents SQL injection by ensuring strings are properly formatted, blocks XSS (Cross-site scripting) attacks by validating and sanitizing input, and stops oversized requests that could crash your server. Joi's built-in methods like `.trim()`, `.escape()`, and length limits automatically clean and validate data, making your app much more secure. +### **Delete** -Validation can also help with one security problem. You do not want users to use trivial passwords. The password checking you'll do is not quite sufficient to protect against that, but it does help. In a production application, you could use an npm package like `check-password-strength`. +`deleteTask` should: -## **4.3 Hashing Passwords** +- Read `req.params.id` +- Find the task index for the current user +- Remove that task from `global.tasks` +- Return the deleted task without `userId` -Never store passwords in the database — you don't need them for authentication. If your database were compromised, or perhaps if an ill-behaved application administrator has a peek at the passwords, you have created a security exposure, which is a problem for the user, and potentially a legal problem for you. +## **4.7 Joi Validation** -Instead, you hash the password and store the hash. When the user sends a logon request, you hash the password the user sends, and compare that with what is stored. If you get a match, the user is authenticated. +Validation means checking incoming data before you store it or use it. -When hashing the password, you should adhere to the following rules: +This project uses Joi. -1. Use a publicly available hashing algorithm and code. Use one that is current and believed to be strong. Never invent your own cryptography: that is extremely hard to get right. Cryptographic algorithms are invented by mathematicians with specialized skills, and each invented algorithm goes through an extensive period of public review by specialists, with extensive testing. You can't match that. Even so, weaknesses are periodically discovered in widely used algorithms, at which time they must be replaced. OWASP, the Open Worldwide Application Security Project, reports that cryptographic weakness is a frequent cause of security exposures. And, just as you shouldn't invent cryptography, you also shouldn't write the code that implements a public algorithm. It is too easy to make a mistake. The publicly available npm packages are what you should use. +Install it in the homework project: -2. For each password, you include a cryptographically chosen salt. Each password has a different salt. This prevents a security exposure called the rainbow attack. +```bash +npm install joi +``` -### **Check for Understanding** +Joi lets you describe the shape of valid data. -1. If a user is securely authenticated, they should be able to access the data they request, correct? +Create: -2. Can you think of any create/update operations that would need only limited validation? +```text +validation/userSchema.js +validation/taskSchema.js +``` -3. Can you think of any security exposures created by limited validation? +### **User Schema** -4. You shouldn't store passwords. What else shouldn't you store? +A user needs: + +- `email`: trimmed, lowercased, valid email, required +- `name`: trimmed, 3 to 30 characters, required +- `password`: required, at least 8 characters, includes uppercase, lowercase, number, and special character + +```js +const Joi = require("joi"); + +const userSchema = Joi.object({ + email: Joi.string().trim().lowercase().email().required(), + name: Joi.string().trim().min(3).max(30).required(), + password: Joi.string() + .trim() + .min(8) + .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z0-9]).+$/) + .required() + .messages({ + "string.pattern.base": + "Password must be at least 8 characters long and include upper and lower case letters, a number, and a special character.", + }), +}); + +module.exports = { userSchema }; +``` + +### **Task Schemas** + +A new task needs a title. If `isCompleted` is not provided, it should default to `false`. + +```js +const Joi = require("joi"); + +const taskSchema = Joi.object({ + title: Joi.string().trim().min(3).max(30).required(), + isCompleted: Joi.boolean().default(false).not(null), +}); + +const patchTaskSchema = Joi.object({ + title: Joi.string().trim().min(3).max(30).not(null), + isCompleted: Joi.boolean().not(null), +}).min(1).message("No attributes to change were specified."); + +module.exports = { taskSchema, patchTaskSchema }; +``` + +The create schema and patch schema are different: + +- Create requires a `title`. +- Patch allows partial updates, but it still needs at least one field. + +### **Using Validation** + +Use `.validate()` before storing data: + +```js +const { error, value } = userSchema.validate(req.body, { + abortEarly: false, +}); +``` + +If `error` exists, return `400`. + +```js +if (error) { + return res.status(400).json({ + message: error.message, + }); +} +``` + +If validation succeeds, use `value`, not the original `req.body`. + +Joi may clean or transform data. For example, it can trim strings or lowercase email addresses. Those cleaned values are in `value`. + +If a request might not have a body, make sure validation receives an object: + +```js +if (!req.body) req.body = {}; +``` + +## **4.8 Password Hashing** + +Never store plain-text passwords. + +If passwords are stored directly and the data is exposed, attackers get the actual passwords. Many people reuse passwords across sites, so that can harm users beyond your app. + +Instead, store a password hash. + +The flow is: + +```text +register -> hash password -> store hashedPassword +logon -> hash submitted password -> compare with stored hash +``` + +This assignment uses Node's built-in `crypto` module. + +`crypto.scrypt` uses callback style, so you will use `util.promisify()` to make it work with `async` and `await`. + +```js +const crypto = require("crypto"); +const util = require("util"); +const scrypt = util.promisify(crypto.scrypt); +``` + +Use these helper functions in `userController.js`: + +```js +async function hashPassword(password) { + const salt = crypto.randomBytes(16).toString("hex"); + const derivedKey = await scrypt(password, salt, 64); + return `${salt}:${derivedKey.toString("hex")}`; +} + +async function comparePassword(inputPassword, storedHash) { + const [salt, key] = storedHash.split(":"); + const keyBuffer = Buffer.from(key, "hex"); + const derivedKey = await scrypt(inputPassword, salt, 64); + return crypto.timingSafeEqual(keyBuffer, derivedKey); +} +``` + +In `register`, store `hashedPassword` instead of `password`. + +```js +const hashedPassword = await hashPassword(password); + +const newUser = { + email, + name, + hashedPassword, +}; +``` + +In `logon`, compare the submitted password with the stored hash. + +```js +const goodCredentials = await comparePassword( + password, + user.hashedPassword, +); +``` + +You do not need to memorize the cryptography details. The key practice is to use trusted crypto tools and never store plain-text passwords. + +## **4.9 Core Check for Understanding** + +1. What is the difference between authentication and authorization? +2. Why is `global.user_id` temporary and not production-ready? +3. Which routes should be protected in Assignment 4? +4. Why should task responses leave out `userId`? +5. Why should controllers use Joi's `value` instead of raw `req.body`? +6. Why should user records store `hashedPassword` instead of `password`? ### **Answers** -1. Not quite - an authenticated user may still attempt to access data they are not authorized to access. +1. Authentication checks who is logged in. Authorization checks whether that user can access a specific resource. +2. It is shared by the whole server, only represents one user at a time, and resets when the server restarts. +3. The task routes should be protected. Register, logon, and logoff should remain public. +4. `userId` is internal ownership data. The client does not need it in task responses. +5. Joi may trim, lowercase, default, or otherwise clean the data. Those cleaned values are in `value`. +6. Plain-text passwords create serious risk if data is exposed. A hash lets the app check passwords without storing the original password. -2. There are some cases where the validation can be less strict (for example: appends to a public forum). +--- -3. We have discussed one security exposure caused by limited validation, which is weak passwords. Another one we'll learn about is an injection attack. The REST call to your API stores a dangerous script. That script is subsequently returned to the front end, where it runs. We'll protect against that in a later lesson. +# **Part 2 — Advanced Knowledge** -4. You shouldn't store credit card numbers. They should only be stored if the application and the entire development and deployment process has been reviewed and approved for PCI DSS (Payment Card Industry Data Security Standards). You shouldn't store bank account numbers. You should avoid, in most cases, storing PII (Personally Identifiable Information). You shouldn't store health data (see [HIPAA standards](https://www.hhs.gov/hipaa/for-professionals/privacy/laws-regulations/index.html)) +Part 2 is optional. It gives deeper context and reference material. + +## **4.10 `Object.assign()` and Patch Updates** + +PATCH means partial update. + +If a task is: + +```js +{ + id: 1, + title: "Write tests", + isCompleted: false, + userId: "jim@sample.com" +} +``` + +And the patch body is: + +```js +{ + isCompleted: true +} +``` + +You do not want to replace the whole task with only `{ isCompleted: true }`. That would lose the ID, title, and owner. + +Instead, mutate the existing task object: + +```js +Object.assign(task, value); +``` + +After that, the stored task is updated in place: + +```js +{ + id: 1, + title: "Write tests", + isCompleted: true, + userId: "jim@sample.com" +} +``` + +Because `task` is the object stored inside `global.tasks`, mutating it updates the stored array entry. + +Still sanitize the response after the update: + +```js +const { userId, ...sanitizedTask } = task; +res.status(200).json(sanitizedTask); +``` + +## **4.11 Password Security Details** + +A password hash is one-way. The app should not be able to turn the hash back into the original password. + +A salt is random data added before hashing. Each user gets a different salt, so two users with the same password should still have different stored hashes. + +This helps protect against precomputed password lookup attacks, often called rainbow table attacks. + +`crypto.timingSafeEqual()` helps compare secret values in a way that avoids leaking information through tiny timing differences. + +The details matter in production, but the rule for this course is simple: + +```text +Use trusted crypto tools. Do not invent your own password storage system. +``` + +Also avoid storing sensitive data your app does not need. Credit card numbers, government ID numbers, and unnecessary personal information create risk and may bring legal requirements. + +## **4.12 Validation Boundaries** + +Validation is one layer of protection. + +Joi can: + +- Require fields +- Check types +- Enforce length rules +- Trim strings +- Lowercase email addresses +- Provide default values + +Joi does not replace authorization. A task can have valid data and still belong to a different user. + +Later, database constraints will add another layer. For now, Joi protects the app before data enters the in-memory arrays. + +## **4.13 Status Code Nuance** + +For this assignment: + +- Use `401` when no user is logged in. +- Use `400` when request data is invalid. +- Use `404` when the requested task is not found for the current user. + +Some APIs use `403 Forbidden` when a user is logged in but not allowed to access a resource. + +This assignment can use `404` for another user's task. That avoids revealing whether the task exists. + +## **4.14 Future Authentication Direction** + +`global.user_id` is only a learning scaffold. + +Later, an app should know which client made the request. Common production patterns include sessions, cookies, and tokens. + +You do not need to implement those in Week 4. The important idea is that the current global login will be replaced later. + +## **4.15 Advanced Check for Understanding** + +1. Why does PATCH usually merge fields instead of replacing the whole object? +2. What does `Object.assign(task, value)` do to the stored task object? +3. What does a salt add to password hashing? +4. Why does validation not replace authorization? +5. Why might an API return `404` instead of `403` for a resource owned by another user? + +### **Answers** +1. PATCH is for partial updates. Replacing the whole object could delete fields that were not included in the request. +2. It mutates the existing task object by copying the fields from `value` onto it. +3. A salt adds unique random data so matching passwords do not produce the same stored hash. +4. Validation checks the shape and values of data. Authorization checks whether this user can access this resource. +5. Returning `404` can avoid revealing whether another user's resource exists. diff --git a/mentor-guidebook/mentor-slides/04-tasks-validations.md b/mentor-guidebook/mentor-slides/04-tasks-validations.md index 7c0792b..7fde682 100644 --- a/mentor-guidebook/mentor-slides/04-tasks-validations.md +++ b/mentor-guidebook/mentor-slides/04-tasks-validations.md @@ -19,7 +19,7 @@ paginate: true --- -# Lesson 4 — Security Middleware, Validation, and Password Hashing +# Lesson 4 - Protected Tasks, Validation, and Password Hashing ## Node.js/Express --- @@ -27,11 +27,12 @@ paginate: true # Game Plan - Warm-Up -- Protected Routes & Auth Middleware -- Data Validation with Joi -- Password Hashing +- Authentication vs authorization +- Protected task routes +- Task ownership +- Joi validation +- Password hashing - Assignment Preview -- Wrap-Up --- @@ -39,329 +40,394 @@ paginate: true In chat or out loud: -1. What clicked for you in Assignment 3? -2. Can you think of a real website where you'd want some routes to be public and others private? +1. What did Assignment 3 add to the todo backend? +2. Which routes in a task app should require a logged-in user? - + --- -# The Problem with Open Routes +# What Week 4 Adds -Right now, anyone can call: +Students keep using the main todo backend. +This week they add: + +- Task routes and task controllers +- Auth middleware for task routes +- Ownership checks so users only access their own tasks +- Joi schemas for user and task request bodies +- Password hashing with Node's built-in `crypto` + +--- + +# Authentication vs Authorization + +Authentication asks: + +> Who is logged in? + +In this assignment, the app uses: + +```js +global.user_id ``` -DELETE /api/tasks/5 -``` -...even without being logged in. +Authorization asks: + +> Is this logged-in user allowed to access this task? + +--- + +# Temporary Login Scaffold + +`global.user_id` is a learning scaffold. -We need a way to protect certain routes. That's what **auth middleware** does. +It lets students practice protected routes before sessions, cookies, tokens, databases, or Prisma. + +Important limitation: + +- It does not identify separate clients +- It is not production-ready auth +- It will be replaced later --- # Auth Middleware Pattern ```js -// middleware/auth.js module.exports = (req, res, next) => { if (!global.user_id) { - return res.status(401).json({ message: "Unauthorized." }); + return res.status(401).json({ message: "Unauthorized" }); } - next(); // logged in — pass to route handler + + next(); }; ``` -Notice: either `return res.json()` **or** `next()` — never both! +Either send a response or call `next()`. + +Do not do both. --- -# Applying Auth Middleware +# Protecting Task Routes -You can protect an entire group of routes at once: +User routes stay public: ```js -const authMiddleware = require("./middleware/auth"); -const taskRouter = require("./routers/taskRoutes"); +app.use("/api/users", userRouter); +``` + +Task routes are protected: -// All task routes require auth +```js app.use("/api/tasks", authMiddleware, taskRouter); ``` -The login and register routes stay **unprotected** — users need to reach them first. +Users need to register and log on before they can access tasks. --- -# DRY: Don't Repeat Yourself - -Without middleware, every route handler would have to check: +# Task Routes -```js -app.get("/api/tasks", (req, res) => { - if (!global.user_id) return res.status(401).json(...); - // ... actual logic -}); +Inside `routes/taskRoutes.js`, paths are relative to `/api/tasks`: -app.post("/api/tasks", (req, res) => { - if (!global.user_id) return res.status(401).json(...); // repeated! - // ... actual logic -}); +```text +POST / -> create +GET / -> index +GET /:id -> show +PATCH /:id -> update +DELETE /:id -> deleteTask ``` -Middleware solves this — write the check once, apply it everywhere. +The tests expect these controller function names. --- -# Quick Think (2 min) +# Task Ownership -If you had a blog with public posts but private drafts, which routes would you protect? +Each stored task needs an internal owner: +```js +userId: global.user_id.email ``` -GET /api/posts (public) -GET /api/posts/:id (public) -POST /api/posts (??? -PATCH /api/posts/:id (??? -DELETE /api/posts/:id (??? -GET /api/drafts (??? -``` - +Stored task shape: + +```js +{ + id: 1, + title: "first task", + isCompleted: false, + userId: "jim@sample.com" +} +``` --- -# Data Validation +# Do Not Return `userId` -Before storing data, **validate** it. +`userId` is for server-side authorization checks. -Bad data leads to: -- Broken behavior -- Security vulnerabilities (injection attacks) -- Unhelpful error messages +The client does not need it in task responses. -We use **Joi** for validation in this project. +Pattern: -```bash -npm install joi +```js +const { userId, ...sanitizedTask } = task; ``` +Use the sanitized copy in `res.json()`. + --- -# Joi Basics +# Controller Flow -```js -const Joi = require("joi"); +For `show`, `update`, and `deleteTask`: -const schema = Joi.object({ - title: Joi.string().min(1).max(255).required(), - isCompleted: Joi.boolean().default(false), -}); +1. Parse `req.params.id` +2. Return `400` if the ID is invalid +3. Find a task matching both ID and `global.user_id.email` +4. Return `404` if no owned task is found +5. Return the task without `userId` -const { error, value } = schema.validate(req.body); -if (error) { - return res.status(400).json({ message: error.details[0].message }); -} +--- + +# Create Flow + +```text +validate body -> create task -> push to global.tasks -> return sanitized task ``` -If validation passes, `value` contains the cleaned data. +The stored task includes `userId`. + +The response does not include `userId`. --- -# Joi Can Transform Too +# Update Flow -```js -const userSchema = Joi.object({ - name: Joi.string().trim().required(), - email: Joi.string().email().lowercase().required(), - password: Joi.string().min(8).required(), -}); +```text +validate patch body -> parse id -> find owned task -> merge fields -> return sanitized task ``` -- `.trim()` — removes leading/trailing whitespace -- `.lowercase()` — normalizes emails -- `.email()` — validates format +Useful pattern: + +```js +Object.assign(task, value); +``` -Validation is your **first line of defense** against bad input. +Core idea: use it to merge validated patch fields into the stored task. --- -# Predict This +# Delete Flow -```js -const schema = Joi.object({ - password: Joi.string().min(8).required(), -}); +Use `findIndex()` because delete needs the array position: -const { error } = schema.validate({ password: "abc" }); +```js +const taskIndex = global.tasks.findIndex(...); ``` -What does `error` contain? What HTTP status should you return? +Then remove the task with: + +```js +global.tasks.splice(taskIndex, 1); +``` - +Copy the task without `userId` before returning it. --- -# Password Storage +# Joi Validation -**Never store plain-text passwords.** +Joi validates request bodies before the app stores data. -If your database is compromised, attackers get every password. +Students create: -Instead: store a **cryptographic hash**. +```text +validation/userSchema.js +validation/taskSchema.js +``` -- A hash is a one-way transformation -- You can verify a password by hashing it and comparing -- Each password gets a unique **salt** to prevent rainbow table attacks +The controller should use the validated `value`, not raw `req.body`. --- -# bcrypt / scrypt +# User Schema -Node's built-in `crypto` module has `scryptSync`: +Rules from the assignment: -```js -const crypto = require("crypto"); +- `email` is required, trimmed, lowercased, and valid email format +- `name` is required, trimmed, and 3 to 30 characters +- `password` is required and must not be trivial -// Hashing (on registration) -const salt = crypto.randomBytes(16).toString("hex"); -const hash = crypto.scryptSync(password, salt, 64).toString("hex"); -const storedHash = `${salt}:${hash}`; +Useful pattern: -// Verifying (on login) -const [savedSalt, savedHash] = storedHash.split(":"); -const hashToCheck = crypto.scryptSync(inputPassword, savedSalt, 64).toString("hex"); -const match = hashToCheck === savedHash; +```js +email: Joi.string().trim().lowercase().email().required() ``` --- -# Security Rules +# Task Schemas + +Create schema: + +- `title` is required +- `isCompleted` defaults to `false` -1. Use a well-known, public hashing algorithm — **never invent your own** -2. Always use a unique salt per password -3. Never store the plain-text password, even temporarily -4. Never store credit card numbers, SSNs, etc. without proper compliance +Patch schema: -> "Never invent your own cryptography." — every security expert ever +- Allows partial updates +- Does not default `isCompleted` +- Requires at least one field + +```js +Joi.object({ ... }).min(1) +``` --- -# We Do — Spot the Bug +# Where Validation Goes -Which of these is safer and why? +Validate near the top of the controller. ```js -// Option A -global.users.push({ ...req.body }); - -// Option B -const { name, email, password } = req.body; -const { error, value } = userSchema.validate({ name, email, password }); -if (error) return res.status(400).json({ message: error.details[0].message }); -global.users.push(value); +const { error, value } = userSchema.validate(req.body, { + abortEarly: false, +}); ``` - +If `error` exists, return `400`. + +If validation passes, build objects from `value`. --- -# We Do — Design a Validation Schema +# Password Hashing + +Never store plain-text passwords. -For this task shape: +Assignment 4 uses Node's built-in modules: ```js -{ - title: "Buy groceries", - isCompleted: false -} +const crypto = require("crypto"); +const util = require("util"); +const scrypt = util.promisify(crypto.scrypt); ``` -What Joi rules would you write? - -- `title` — required? max length? what type? -- `isCompleted` — required? default value? - - +`crypto.scrypt` uses callback style. `util.promisify()` lets students use `await`. --- -# You Do (5 min) +# Register With Hashing -Write a Joi schema for a task `update` operation: +In `register`: -- `title` should be optional (you might only update one field) -- `isCompleted` should be optional but must be boolean if present -- Reject any unknown fields +1. Validate `req.body` +2. Use the validated `value` +3. Hash the password +4. Store `hashedPassword` +5. Do not store plain `password` -**Hint:** `Joi.object({ ... }).options({ allowUnknown: false })` +Stored user shape: - +```js +{ email, name, hashedPassword } +``` --- -# Assignment Preview +# Logon With Hashing -This week's assignment is the biggest one so far: +In `logon`: -1. Build all 5 task routes: create, index, show, update, deleteTask -2. Add auth middleware in front of all task routes -3. Add Joi validation for user and task create/update -4. Hash passwords on register (using `crypto.scryptSync`) -5. Verify hashed password on login +1. Find the user by email +2. Compare the submitted password with `user.hashedPassword` +3. Return `401` if credentials do not match +4. Set `global.user_id` when login succeeds -The assignment is labeled "a lot of work" — give yourself extra time! +Do not compare against `user.password` after this change. --- -# Assignment: Task ID Pattern +# Status Code Guide -```js -const taskCounter = (() => { - let lastTaskNumber = 0; - return () => { - lastTaskNumber += 1; - return lastTaskNumber; - }; -})(); -``` +For this assignment: + +- `200`: successful read, update, delete, or logon/logoff +- `201`: successful register or task create +- `400`: invalid request body or invalid task ID +- `401`: no logged-in user or invalid credentials +- `404`: no task found for this user + +--- + +# Assignment Preview -This is a **closure** — a function that remembers its own state. +Students will: -Each call to `taskCounter()` returns the next unique ID. +- Add `controllers/taskController.js` +- Add `routes/taskRoutes.js` +- Add `middleware/auth.js` +- Add `validation/userSchema.js` +- Add `validation/taskSchema.js` +- Update `app.js` +- Update `controllers/userController.js` --- -# Wrap-Up +# Tests -In chat: +Run the required Week 4 test: -1. Why do we use middleware for auth instead of putting the check in every route? -2. Why should we never store a plain-text password? -3. What's the difference between a validation error (400) and an unauthorized error (401)? +```bash +npm run tdd assignment4a +``` + +`assignment4a` checks protected task behavior, basic validation, and password hashing. + +If students attempt the optional advanced section, also run: + +```bash +npm run tdd assignment4b +``` + +`assignment4b` checks deeper validation, patch update, and password security behavior. --- -# Confidence Check +# Advanced Knowledge -On a scale of 1–5: +Use these if time allows: -How confident are you about building protected routes this week? +- `Object.assign()` mutates the stored task object +- Salted password hashes protect against precomputed password attacks +- Joi validation does not replace authorization +- Some APIs use `403`, but this assignment can use `404` for another user's task +- Real auth later uses sessions, cookies, or tokens --- -# Resources +# Wrap-Up -- https://joi.dev/api/ -- https://nodejs.org/api/crypto.html -- Ask questions in Slack +In chat: + +1. What is the difference between authentication and authorization? +2. Why should task responses leave out `userId`? +3. Why should controllers use Joi's validated `value`? +4. Why should passwords be hashed before storage? --- # Closing -**This week:** -Auth middleware, Joi validation, and proper password storage. +This week: + +Protected task routes, task ownership, validation, and password hashing. -**Next week:** -We switch from in-memory arrays to a real database — SQL and PostgreSQL. +Next week: -See you then! +SQL and PostgreSQL replace temporary in-memory storage. diff --git a/mentor-guidebook/mentors-session-notes/lesson4.md b/mentor-guidebook/mentors-session-notes/lesson4.md index 4542c99..5740d66 100644 --- a/mentor-guidebook/mentors-session-notes/lesson4.md +++ b/mentor-guidebook/mentors-session-notes/lesson4.md @@ -1,77 +1,196 @@ -# Lesson/Assignment 4: Authentication Middleware, Tasks, Validation +# Lesson/Assignment 4: Protected Tasks, Validation, and Password Hashing -## Task Request Handler Methods +## Session focus -We need the following (the standard CRUD) -- create -- show -- index -- update -- deleteTask -Note: “delete” is a reserved word in JavaScript. +Keep the session centered on extending the main todo backend from Assignment 3. Week 4 is where students add private task data, validation, and safer password storage while still using the temporary in-memory scaffold. -All of these have parameters (req, res) and usually next. Any of these may be declared as async, but we aren’t using a database yet, so we don’t need async/await in these functions yet. Your assignment shows a closure. Try to understand that code – it’s a good trick to know, and a standard interview question. +## Core + advanced optional structure -## Storing/modifying Tasks +Lesson 4 is split into **Core Knowledge** and **Advanced Knowledge**. Core topics are required for the app to work: authentication vs authorization, limits of `global.user_id`, protected task routes, task ownership, task CRUD controllers, Joi validation, and password hashing. Advanced topics are optional context: deeper `Object.assign()` behavior, password security details, validation boundaries, status code nuance, and future auth direction. -We are using global.tasks for now, because we aren’t yet using a database. This is an array. You can add a new entry with global.tasks.push(). Each task has to correspond to a user, so we use task.userId = global.user_id.email. +The advanced section should not change the required app flow. If time is short, keep students focused on the core tasks. -Be careful how you mutate these entries. You need to modify a task objects before passing them to res.json(), so that you don’t include the userId. But you better not modify the ones in global.tasks. So you need to make a copy. On the other hand, update() modifies the task in place. You use Object.apply(). You use global.tasks.splice() to delete a task. +## Core concepts to emphasize -## Authentication and Access Control for Tasks +- Authentication answers "who is logged in?" +- Authorization answers "is this user allowed to access this task?" +- `global.user_id` is a temporary learning scaffold, not production auth. +- User routes stay public so users can register and log on. +- Task routes are protected with `authMiddleware`. +- Every stored task needs `userId: global.user_id.email`. +- Every task lookup for `show`, `update`, and `deleteTask` must check both task ID and `global.user_id.email`. +- Task responses should not include `userId`. +- Joi validation should happen before storing or updating data. +- Controllers should use Joi's cleaned `value`, not raw `req.body`. +- Passwords should be stored as `hashedPassword`, not plain `password`. -Each task belongs to a user. You need to be sure that a user is logged in before any task controller method is called, else a 401 should be sent back. You use middleware for this. That solves the authentication problem. +## Route and file reminders -For access control, you need to be sure that no task object is accessed either for reading or writing unless it belongs to the logged on user. +Required files: -For index(), you can use global.tasks.filter(). This gives you an array of tasks – but, be careful, the entries in this list are references to tasks in global.tasks, so you have to be cautious in how these are mutated. Every task method has to filter on userId. +```text +controllers/taskController.js +routes/taskRoutes.js +middleware/auth.js +validation/userSchema.js +validation/taskSchema.js +``` -This access control is completely insecure now, because once global.user_id is set, any user can access the corresponding tasks. +Task router paths are relative to `/api/tasks`: -## Validation +```text +POST / -> create +GET / -> index +GET /:id -> show +PATCH /:id -> update +DELETE /:id -> deleteTask +``` -You don’t want bogus entries to be stored. You use Joi validation. You define the schemas for create and patch. +In `app.js`, the task router should be mounted with auth middleware: -Before you do each validation, you need to check if req.body is undefined. If you pass undefined to Joi validation, it does not report an error, but the value that it returns is undefined – not what you want. Your could could do: ```js -if (!req.body) req.body = {}; +app.use("/api/tasks", authMiddleware, taskRouter); ``` -Then Joi will recognize the error. -Before you do show(), update(), or delete(), you need to get the id from req.params and convert it to a number. So you need a check like: -```js -const id = parseInt(req.params?.id); -if (!id) { // might be NaN - return res.status(400).json(message: “No valid id passed.”); -} -``` +Do not put auth middleware in front of user routes. -If Joi validation fails, you need to return a 400, an error message, and details on the failure. You could do this in your request handler as follows: -```js - if (error) { - return res.status(400).json({ message: "Validation failed", - details: error.details, - }); -``` -But, this doesn’t follow the DRY principle: Don’t Repeat Yourself. You would repeat this exact code for user register and task create/update. A better way is this: +## Task controller guidance -```js -if error return next(error); // this throws it to the global error handler -``` +For task creation: + +- Validate `req.body` with `taskSchema`. +- Use `value` from Joi. +- Store `id`, `userId`, `title`, and `isCompleted`. +- Return status `201`. +- Return the task without `userId`. + +For `index`: + +- Filter tasks by `global.user_id.email`. +- Return `404` if there are no tasks for this user. +- Return sanitized tasks without `userId`. + +For `show`: + +- Parse `req.params.id`. +- Return `400` for an invalid ID. +- Find a task matching the ID and logged-in user's email. +- Return `404` if no owned task exists. + +For `update`: + +- Validate with `patchTaskSchema`. +- Parse the ID. +- Find the owned task. +- Use `Object.assign(task, value)` to merge validated patch fields. +- Return the sanitized updated task. + +For `deleteTask`: + +- Parse the ID. +- Use `findIndex()` with both task ID and logged-in user's email. +- Copy the task without `userId`. +- Remove it with `splice()`. +- Return the sanitized deleted task. + +## Joi validation guidance + +User schema rules: + +- `email`: required, trimmed, lowercased, valid email format +- `name`: required, trimmed, 3 to 30 characters +- `password`: required and not trivial + +Task schema rules: + +- `taskSchema.title`: required, trimmed, 3 to 30 characters +- `taskSchema.isCompleted`: boolean, defaults to `false`, not `null` +- `patchTaskSchema.title`: optional, trimmed, 3 to 30 characters, not `null` +- `patchTaskSchema.isCompleted`: optional boolean, not `null` +- `patchTaskSchema`: requires at least one field with `.min(1)` + +Remind students that create and patch need different schemas. The patch schema should not add the `isCompleted: false` default when the user only updates a title. + +## Password hashing guidance + +Use Node's built-in `crypto` and `util` modules: -Then, in your global error handler, you would add code like this: ```js - if (err.name === "ValidationError") { - return res.status(400).json({ message: "Validation failed", - details: err.details, - }); +const crypto = require("crypto"); +const util = require("util"); +const scrypt = util.promisify(crypto.scrypt); ``` -Be careful with your Joi patch schema! The default value for isCompleted is false, but you don’t want that default in your patchTaskSchema, or it will add that to each update! -## Hashing Passwords +Core idea: + +- `hashPassword(password)` creates a salt and stored hash. +- `comparePassword(inputPassword, storedHash)` checks login attempts. +- `register` stores `hashedPassword`. +- `logon` compares the submitted password to `user.hashedPassword`. + +Students do not need to memorize cryptography internals. The practical rule is: use trusted crypto tools and never store plain-text passwords. + +## Status code guidance -A cryptographic hash is a one way function. It is computationally infeasible to derive the plaintext from the hash – at least if you use strong cryptography. But, given some plaintext, you always get the same hash. Never store passwords, always just the hash. +For Assignment 4: -The salt is a cryptographically random string, which is concatenated with the password before computing the hash. Then the salt is concatenated with the hash and both are stored. The process can be repeated at logon time to check the password. +- `200 OK`: successful logon/logoff, read, update, or delete +- `201 Created`: successful register or task creation +- `400 Bad Request`: invalid body or invalid task ID +- `401 Unauthorized`: no logged-in user or invalid credentials +- `404 Not Found`: no matching task for this user + +Some APIs use `403 Forbidden` when a logged-in user cannot access a resource. This assignment can use `404` for another user's task so the API does not reveal whether that task exists. + +## Assignment 4 reminders + +Part A focuses on required Week 4 behavior: + +- auth middleware +- user register/logon/logoff still working +- task create/index/show/update/delete +- task ownership checks +- task responses without `userId` +- invalid task IDs returning `400` +- basic validation and password hashing checks + +Part B is the optional advanced test for deeper validation, patch update, and password security: + +- user schema rules +- task schema and patch schema rules +- controllers using validation before storage +- patch updates merging fields without replacing the stored task +- password hashing in `register` +- password comparison in `logon` + +Commands: + +```bash +npm run tdd assignment4a +npm run tdd assignment4b # optional advanced +``` -Why you need the salt: In the rainbow attack, the attacker somehow gets access to all the hashed passwords. Then they compare these hashes against hashes of the 10000 most commonly used passwords, and as often as not they’ll get a match. The salt makes this harder. They’d need to compute the hashes of each of the salt values concatenated with each of the 10000 most used passwords – a much more time consuming project. +## Common student issues + +- Using `routers/taskRoutes` instead of `routes/taskRoutes`. +- Protecting user routes, which prevents registration and logon. +- Forgetting to call `next()` in `authMiddleware`. +- Calling `next()` after sending a `401`. +- Checking only task ID and forgetting to check `global.user_id.email`. +- Returning `userId` in task responses. +- Using raw `req.body` instead of Joi's validated `value`. +- Adding `.default(false)` to `patchTaskSchema`. +- Storing `password` after adding `hashedPassword`. +- Comparing login password to `user.password` instead of `user.hashedPassword`. + +## Debugging prompts + +When students are stuck, ask: + +1. Is `global.user_id` set to the logged-in user? +2. Did the request reach the controller or stop in middleware? +3. Is the task stored with the logged-in user's email? +4. Does the lookup check both task ID and owner? +5. Did Joi return an `error`? +6. Is the controller using Joi's `value`? +7. Does the stored user have `hashedPassword` instead of `password`? diff --git a/mentor-guidebook/sample-answers/assignment4/validation/taskSchema.js b/mentor-guidebook/sample-answers/assignment4/validation/taskSchema.js index 39c230e..6ca0c64 100644 --- a/mentor-guidebook/sample-answers/assignment4/validation/taskSchema.js +++ b/mentor-guidebook/sample-answers/assignment4/validation/taskSchema.js @@ -1,13 +1,13 @@ const Joi = require("joi"); const taskSchema = Joi.object({ - title: Joi.string().required(), - isCompleted: Joi.boolean().default(false), + title: Joi.string().trim().min(3).max(30).required(), + isCompleted: Joi.boolean().default(false).not(null), }); const patchTaskSchema = Joi.object({ - title: Joi.string().optional(), - isCompleted: Joi.boolean().optional(), -}); + title: Joi.string().trim().min(3).max(30).not(null), + isCompleted: Joi.boolean().not(null), +}).min(1).message("No attributes to change were specified."); module.exports = { taskSchema, patchTaskSchema }; diff --git a/mentor-guidebook/sample-answers/assignment4/validation/userSchema.js b/mentor-guidebook/sample-answers/assignment4/validation/userSchema.js index 4c9e0d7..9f41de9 100644 --- a/mentor-guidebook/sample-answers/assignment4/validation/userSchema.js +++ b/mentor-guidebook/sample-answers/assignment4/validation/userSchema.js @@ -1,15 +1,16 @@ const Joi = require("joi"); const userSchema = Joi.object({ - name: Joi.string().min(3).max(30).required(), - email: Joi.string().email().required(), + email: Joi.string().trim().lowercase().email().required(), + name: Joi.string().trim().min(3).max(30).required(), password: Joi.string() + .trim() .min(8) - .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/) + .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z0-9]).+$/) .required() .messages({ "string.pattern.base": - "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character", + "Password must be at least 8 characters long and include upper and lower case letters, a number, and a special character.", }), });