USER
4. Implement authentication logic using JWT strategy (POST
/login endpoint).
a. Use the jsonwebtoken library for JWT operations.
b. JWT should be implemented with access and refresh
tokens.
c. Tokens should not be stored in the database.
5. Unauthorized users should be redirected to login if they are
unauthorized or if tokens are expired.
a. If the access token is expired (server sends a 401 status
error) but the refresh token is valid, then the frontend
should automatically refresh the access token and
repeat failed requests.
6. The endpoint for getting projects should be protected by
JWT.
У меня есть вот такой набор заданий к моему клиент-серверному приложению, которое использует React/Redux/ExpressJS.
Я начал вводить логику работы с JWT токенами в код, но сбился с пути и забыл про ход своих мыслей, я буду отправлять тебе код, а ты помоги мне реализовать все эти задания.
Сервер:
import express from "express";
import dotenv from "dotenv";
import cors from "cors";
import pkg from "pg";
const { Client } = pkg;
import bcrypt from "bcrypt";
import knex from "knex";
import jwt from "jsonwebtoken";
dotenv.config();
const postgres = knex({
client: "pg",
connection: process.env.LOCALDB_URL,
});
const port = process.env.PORT || 5000;
const app = express();
app.use(cors());
app.use("/img", express.static("./img"));
app.use(express.json());
app.get("/api/test", (_, res) => {
res.json({ message: "Server connected to frontend" });
});
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;
let refreshTokens = [];
function authenticateToken(req, res, next) {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (!token) return res.status(401).json({ message: "Token not provided" });
jwt.verify(token, ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return res.status(401).json({ message: "Invalid tokn" });
req.user = user;
next();
});
}
app.post("/api/login", async (req, res) => {
const { username, password } = req.body;
try {
const user = await postgres("users").where({ username }).first();
if (!user) {
return res.status(401).json({ message: "Неверные учетные данные" });
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ message: "Неверные учетные данные" });
}
const userPayload = {
userId: user.id,
username: user.username,
};
const accessToken = jwt.sign(userPayload, ACCESS_TOKEN_SECRET, {
expiresIn: "15m",
});
const refreshToken = jwt.sign(userPayload, REFRESH_TOKEN_SECRET, {
expiresIn: "7d",
});
refreshTokens.push(refreshToken);
res.status(200).json({
accessToken,
refreshToken,
user: {
userId: user.id,
username: user.username,
},
});
} catch (error) {
console.error("Ошибка при логине:", error);
res.status(500).json({ message: "Ошибка сервера" });
}
});
app.post("/api/token", (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken)
return res.status(401).json({ message: "Token not provided" });
if (!refreshTokens.includes(refreshToken))
return res.status(403).json({ message: "Invalid token" });
jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, user) => {
if (err) return res.status(403).json({ message: "Invalid Token" });
const userPayload = {
userId: user.userId,
username: user.username,
};
const newAccessToken = jwt.sign(userPayload, ACCESS_TOKEN_SECRET, {
expiresIn: "15m",
});
res.json({ accessToken: newAccessToken });
});
});
app.post("/api/logout", (req, res) => {
const { refreshToken } = req.body;
refreshTokens = refreshTokens.filter((token) => token !== refreshToken);
res.status(200).json({ message: "User quit app" });
});
app.get("/api/cards", authenticateToken, async (req, res) => {
try {
const searchTerm = req.query.search ? req.query.search.toLowerCase() : "";
let projectsQuery = postgres("projects").select("*");
if (searchTerm) {
projectsQuery = projectsQuery.where(function () {
this.whereRaw("LOWER(title) LIKE ?", [`%${searchTerm}%`]).orWhereRaw(
"LOWER(description) LIKE ?",
[`%${searchTerm}%`]
);
});
}
const projects = await projectsQuery;
const apiBaseUrl =
process.env.LOCALSERVER ?? `https://server-ancient-grass-9030.fly.dev`;
const projectsWithImgSrc = projects.map((project) => ({
...project,
imgSrc: `${apiBaseUrl}${project.img_src}`,
}));
res.json(projectsWithImgSrc);
} catch (error) {
console.error("Error fetching projects:", error);
res.status(500).json({ error: "Failed to fetch projects" });
}
});
app.post("/api/signup", async (req, res) => {
const { firstName, lastName, username, password, confirmPassword, age } =
req.body;
const errors = {};
if (!firstName || firstName.trim().length < 3) {
errors.firstName = "First name must be at least 3 characters long.";
}
if (!lastName || lastName.trim().length < 3) {
errors.lastName = "Last name must be at least 3 characters long.";
}
if (!username || username.trim().length < 3) {
errors.username = "Username must be at least 3 characters long.";
}
if (!password) {
errors.password = "Please enter a password.";
} else {
if (password.length < 4) {
errors.password = "Password must be at least 4 characters long.";
}
if (!/[a-zA-Z]/.test(password) || !/[0-9]/.test(password)) {
errors.password =
"Password must contain at least one letter and one number.";
}
}
if (!confirmPassword) {
errors.confirmPassword = "Please confirm your password.";
} else if (password !== confirmPassword) {
errors.confirmPassword = "Passwords do not match.";
}
const ageNumber = Number(age);
if (!age) {
errors.age = "Please specify your age.";
} else if (isNaN(ageNumber)) {
errors.age = "Age must be a number.";
} else if (ageNumber <= 0) {
errors.age = "Age cannot be zero or negative.";
}
if (Object.keys(errors).length > 0) {
return res.status(400).json({ errors });
}
try {
const existingUser = await postgres("users").where({ username }).first();
if (existingUser) {
return res
.status(400)
.json({ errors: { username: "Username is already taken." } });
}
const hashedPassword = await bcrypt.hash(password, 12);
await postgres("users").insert({
firstName,
lastName,
username,
password: hashedPassword,
age: ageNumber,
});
res.status(201).json({ message: "User registered successfully" });
} catch (error) {
console.error("Error during registration:", error);
res.status(500).json({ message: "Server error" });
}
});
app.listen(port, () => console.log(`Server is running on port ${port}`));
const client = new Client({
connectionString: process.env.LOCALDB_URL,
});
async function connectToDatabase() {
try {
await client.connect();
console.log("Successfully connected to the database");
} catch (error) {
console.error("Error connecting to the database:", error);
} finally {
await client.end();
}
}
connectToDatabase();
Файл со всеми экшенами:
import {
login,
logout,
searchTerm as search,
loginError as logErr,
} from "./types";
export const loginThunk = (username, password) => async (dispatch) => {
try {
const response = await fetch("http://localhost:5000/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (response.ok) {
const { accessToken, refreshToken, user } = data;
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
localStorage.setItem("user", JSON.stringify(user));
dispatch(loginAction(data));
return { success: true };
} else {
dispatch(loginError(data.message));
return { success: false, message: data.message };
}
} catch (error) {
dispatch(loginError(error.toString()));
return { success: false, message: error.toString() };
}
};
export const setSearchTerm = (searchTerm) => {
return {
type: search,
value: searchTerm,
};
};
export const logoutAction = () => {
return {
type: logout,
};
};
export const loginError = (message) => {
return {
type: logErr,
value: message,
};
};
export const loginAction = (user) => {
return {
type: login,
value: user,
};
};
export const fetchSearchItems = (searchTerm) => {
return async (dispatch) => {
try {
const response = await fetch(
`http://localhost:5000/api/cards?search=${searchTerm}`
);
const data = await response.json();
dispatch({
type: "set_filtered_objects",
value: data,
});
} catch (error) {
console.log("Happened error: \n", error);
}
};
};
export const fetchAllItems = () => {
return async (dispatch) => {
try {
const response = await fetch(`http://localhost:5000/api/cards`);
const data = await response.json();
dispatch({
type: "set_filtered_objects",
value: data,
});
} catch (error) {
console.log("Happened error: \n" + error);
}
};
};
Файл редюсеров:
import {
login,
logout,
loginError,
searchTerm,
setFiltered,
} from "./actions/types";
const defaultState = {
searchTerm: "",
filteredObjects: [],
};
export const reducer = (state = defaultState, action) => {
switch (action.type) {
case searchTerm:
return { ...state, searchTerm: action.value };
case setFiltered:
return { ...state, filteredObjects: action.value };
default:
return state;
}
};
const initialState = {
isAuth: false,
user: null,
};
export const userReducer = (state = initialState, action) => {
switch (action.type) {
case login:
return {
...state,
isAuth: true,
user: action.payload,
};
case logout:
return {
...state,
isAuth: false,
user: null,
};
case loginError:
return { ...state, isAuth: false, user: null };
default:
return state;
}
};
Login.jsx:
import logo from "../../../public/assets/images/spring.png";
import LoginInput from "./loginInput/loginInput";
import LoginButton from "./loginButton/LoginButton";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { loginThunk } from "../../store/actions/actions";
export default function Login() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState({ error: "" });
const dispatch = useDispatch();
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
setError({ error: "" });
const result = await dispatch(loginThunk(username, password));
if (result.success) {
navigate("/");
} else {
setError({ error: result.message });
}
};
const handleSignupRedirect = (e) => {
e.preventDefault();
navigate("/signup");
};
return (
<div className="login">
<div className="login__header">
<img className="login__header__image" src={logo} alt="logo_icon" />
</div>
<div className="login__card">
<div className="login__card__header">
<span className="login__card__header__sentence">
Please, Enter your details
</span>
<h4 className="login__card__header__title">Welcome</h4>
</div>
<form className="login__card__info" onSubmit={handleSubmit}>
<div className="login__card__info__inputs">
<LoginInput
type="text"
value={username}
placeholder="Enter your username"
onChange={(e) => setUsername(e.target.value)}
/>
<LoginInput
type="password"
value={password}
placeholder="Enter your password"
onChange={(e) => setPassword(e.target.value)}
/>
<span className="login__error">{error.error}</span>
</div>
<div className="login__card__info__buttons">
<LoginButton type="submit" label="Submit" />
<LoginButton
type="button"
label="Create an account"
onClick={handleSignupRedirect}
/>
</div>
</form>
</div>
</div>
);
}
Файл, который я назвал jwtFunctions:
import { logoutAction } from "../store/actions/actions";
async function fetchWithAuth(url, options = {}) {
let accessToken = localStorage.getItem("accessToken");
if (!options.headers) {
options.headers = {};
}
options.headers["Authorization"] = `Bearer ${accessToken}`;
let response = await fetch(url, options);
if (response.status === 401) {
const refreshToken = localStorage.getItem("refreshToken");
const tokenResponse = await fetch("http://localhost:5000/api/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken }),
});
if (tokenResponse.ok) {
const tokenData = await tokenResponse.json();
accessToken = tokenData.accessToken;
localStorage.setItem("accessToken", accessToken);
options.headers["Authorization"] = `Bearer ${accessToken}`;
response = await fetch(url, options);
} else {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("user");
window.location.href = "/login";
return;
}
}
return response;
}
export const getCards = () => async (dispatch) => {
try {
const response = await fetchWithAuth("http://localhost:5000/api/cards");
if (response.ok) {
const data = await response.json();
dispatch(setCards(data));
} else {
const errorData = await response.json();
console.error("Ошибка при получении карточек:", errorData);
}
} catch (error) {
console.error("Ошибка при получении карточек:", error);
}
};
export const logoutUser = () => async (dispatch) => {
const refreshToken = localStorage.getItem("refreshToken");
await fetch("http://localhost:5000/api/logout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken }),
});
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
dispatch(logoutAction());
//redirect
window.location.href = "/login";
};
ASSISTANT
It seems you're close to a full implementation, but there are some aspects that need to be adjusted to fully meet the requirements specified in your tasks.
Let's go through each of the tasks to see what's needed, and I'll guide you through the necessary changes.
---
### Task 4: Implement authentication logic using JWT strategy (POST /login endpoint).
You've already done a good job implementing the `/api/login` endpoint with access and refresh tokens using the `jsonwebtoken` library, and you've ensured that tokens are not stored in the database. So Task 4 is essentially completed.
---
### Task 5: Unauthorized users should be redirected to login if they are unauthorized or if tokens are expired.
The critical part here is ensuring that on the **frontend**, when an endpoint returns a 401 status (unauthorized), the application attempts to refresh the access token using the refresh token. If the refresh token is valid and the server issues a new access token, the frontend should retry the failed request automatically.
If the refresh token is invalid or expired, then the user should be redirected to the login page.
Let me help you adjust your code to meet this requirement.
---
#### Adjusting the `fetchWithAuth` Function
Your `fetchWithAuth` function is on the right track, but it needs some tweaks to ensure it retries the original request after refreshing the access token.
Here's how you can adjust it:
```javascript
// jwtFunctions.js
export async function fetchWithAuth(url, options = {}) {
let accessToken = localStorage.getItem("accessToken");
if (!options.headers) {
options.headers = {};
}
options.headers["Authorization"] = `Bearer ${accessToken}`;
let response = await fetch(url, options);
if (response.status === 401) {
// Try to refresh the access token
const refreshToken = localStorage.getItem("refreshToken");
const tokenResponse = await fetch("http://localhost:5000/api/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken }),
});
if (tokenResponse.ok) {
const tokenData = await tokenResponse.json();
accessToken = tokenData.accessToken;
localStorage.setItem("accessToken", accessToken);
// Update the authorization header
options.headers["Authorization"] = `Bearer ${accessToken}`;
// Retry the original request with the new access token
response = await fetch(url, options);
} else {
// Refresh token is invalid or expired - log out the user
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("user");
window.location.href = "/login";
return Promise.reject(new Error("Unauthorized"));
}
}
return response;
}
```
**Explanation of Adjustments:**
- **Retrying the Original Request:**
- After obtaining a new access token, we need to retry the original request.
- This is done by reassigning `response` with the result of `await fetch(url, options);` after updating the `Authorization` header with the new access token.
- **Handling Unauthorized Cases:**
- If the refresh token is invalid or expired, we clear the tokens from `localStorage` and redirect the user to the login page.
- **Promise Rejection:**
- We return `Promise.reject(new Error("Unauthorized"))` to let the calling function know that the request failed due to authorization issues.
---
#### Updating Action Creators to Use `fetchWithAuth`
In your action creators (`fetchAllItems` and `fetchSearchItems`), you are currently using the regular `fetch` function. Since `/api/cards` is a protected endpoint, and we need to handle token refreshing logic, you should use `fetchWithAuth` instead.
Here's how you can adjust your action creators:
```javascript
// actions.js
import { fetchWithAuth } from "../../utils/jwtFunctions"; // Adjust the import path accordingly
export const fetchAllItems = () => {
return async (dispatch) => {
try {
const response = await fetchWithAuth(`http://localhost:5000/api/cards`);
if (response.ok) {
const data = await response.json();
dispatch({
type: "set_filtered_objects",
value: data,
});
} else {
// Handle non-OK responses
console.error("Error fetching all items:", response.statusText);
}
} catch (error) {
console.log("An error occurred:\n", error);
}
};
};
export const fetchSearchItems = (searchTerm) => {
return async (dispatch) => {
try {
const response = await fetchWithAuth(
`http://localhost:5000/api/cards?search=${searchTerm}`
);
if (response.ok) {
const data = await response.json();
dispatch({
type: "set_filtered_objects",
value: data,
});
} else {
// Handle non-OK responses
console.error("Error fetching search items:", response.statusText);
}
} catch (error) {
console.log("An error occurred:\n", error);
}
};
};
```
**Explanation:**
- **Use `fetchWithAuth`:**
- We replace `fetch` with `fetchWithAuth` to ensure that the authorization header is set, and token refreshing logic is handled.
- **Response Handling:**
- We check if the response is `ok` (status in the range 200-299). If not, we log an error.
- This ensures we only proceed when the server returns a successful response.
---
#### Adjusting the Logout Functionality
In your `logoutUser` function within `jwtFunctions`, you should dispatch the `logoutAction` after clearing the tokens, and then redirect the user.
Here's how you can adjust it:
```javascript
// jwtFunctions.js
import { logoutAction } from "../store/actions/actions";
export const logoutUser = () => async (dispatch) => {
const refreshToken = localStorage.getItem("refreshToken");
try {
await fetch("http://localhost:5000/api/logout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken }),
});
} catch (error) {
console.error("Error during logout:", error);
} finally {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("user");
dispatch(logoutAction());
// Redirect to login page
window.location.href = "/login";
}
};
```
**Explanation:**
- **Try-Catch Block:**
- We wrap the logout request in a `try-catch` block to handle any network errors.
- **Clearing Local Storage:**
- We ensure all related items (`accessToken`, `refreshToken`, `user`) are cleared.
- **Dispatching Logout Action:**
- Dispatch the `logoutAction` to update the Redux state accordingly.
---
### Task 6: The endpoint for getting projects should be protected by JWT.
From your server code, you've already protected the `/api/cards` endpoint with the `authenticateToken` middleware.
However, on the frontend, ensure that all requests to this endpoint are using `fetchWithAuth` to correctly include the `Authorization` header with the access token.
As shown above, we've updated `fetchAllItems` and `fetchSearchItems` to use `fetchWithAuth`.
---
### Additional Adjustments
#### Handling Unauthorized Routes on the Frontend
To ensure that unauthorized users are redirected to the login page when they try to access protected routes, you need to implement route protection in your React application.
You can create a higher-order component (HOC) or use React Router's capabilities to protect routes.
Here's a simple way to do this using React Router:
```jsx
// PrivateRoute.jsx
import React from "react";
import { Route, Navigate } from "react-router-dom";
export default function PrivateRoute({ element: Component, ...rest }) {
const isAuthenticated = localStorage.getItem("accessToken") !== null;
return isAuthenticated ? (
<Route {...rest} element={<Component />} />
) : (
<Navigate to="/login" />
);
}
```
**Usage in Your Routing Configuration:**
```jsx
// App.jsx or wherever your routes are configured
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import PrivateRoute from "./PrivateRoute";
import HomePage from "./components/HomePage";
import LoginPage from "./components/LoginPage";
// ... other imports
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<LoginPage />} />
<PrivateRoute path="/" element={<HomePage />} />
{/* ... other routes */}
</Routes>
</Router>
);
}
```
**Note:**
- Update the component definitions to match your actual components.
- This setup ensures that if a user is not authenticated (no `accessToken` in `localStorage`), they are redirected to the `/login` page.
---
#### Ensuring Consistency in Reducers
Looking at your reducers, there might be some inconsistencies.
In your `userReducer`, you're not handling the `loginError` action properly.
Here's how you can adjust it:
```javascript
// reducers.js
const initialState = {
isAuth: false,
user: null,
loginError: null,
};
export const userReducer = (state = initialState, action) => {
switch (action.type) {
case login:
return {
...state,
isAuth: true,
user: action.value, // Adjusted from action.payload to action.value
loginError: null,
};
case logout:
return {
...state,
isAuth: false,
user: null,
loginError: null,
};
case loginError:
return {
...state,
isAuth: false,
user: null,
loginError: action.value, // Store the error message
};
default:
return state;
}
};
```
**Explanation:**
- **Store Error Messages:**
- By storing the `loginError` in the state, you can display error messages in your components.
- **Consistency with Action Creators:**
- Ensure that the action properties (`action.value` or `action.payload`) match between your action creators and reducers.
---
#### Adjusting the `loginThunk` Action Creator
Ensure that the `loginThunk` action creator dispatches the correct data.
```javascript
// actions.js
export const loginThunk = (username, password) => async (dispatch) => {
try {
const response = await fetch("http://localhost:5000/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (response.ok) {
const { accessToken, refreshToken, user } = data;
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
localStorage.setItem("user", JSON.stringify(user));
dispatch(loginAction(user)); // Pass the user data
return { success: true };
} else {
dispatch(loginError(data.message));
return { success: false, message: data.message };
}
} catch (error) {
dispatch(loginError(error.toString()));
return { success: false, message: error.toString() };
}
};
```
**Explanation:**
- **Pass Correct Data to Actions:**
- When dispatching `loginAction`, ensure that you're passing the user data (`user`) as `action.value` (since in your reducer, you're accessing `action.value`).
---
#### Adjusting `Login.jsx`
In your `Login.jsx` component, you should handle the `loginError` from the Redux state instead of using local component state.
Here's how you can adjust it:
```jsx
// Login.jsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { loginThunk } from "../../store/actions/actions";
export default function Login() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const dispatch = useDispatch();
const navigate = useNavigate();
const loginError = useSelector((state) => state.userReducer.loginError);
const handleSubmit = async (e) => {
e.preventDefault();
const result = await dispatch(loginThunk(username, password));
if (result.success) {
navigate("/");
}
// No need to handle error here; it's managed by Redux state
};
const handleSignupRedirect = (e) => {
e.preventDefault();
navigate("/signup");
};
return (
<div className="login">
{/* ... rest of your component */}
<div className="login__card__info__inputs">
{/* ... your inputs */}
{loginError && <span className="login__error">{loginError}</span>}
</div>
{/* ... rest of your component */}
</div>
);
}
```
**Explanation:**
- **Use `useSelector` to Get `loginError`:**
- By accessing `loginError` from the Redux state, you ensure consistent error handling across your application.
- **Remove Local `error` State:**
- Since error messages are managed in the Redux state, you don't need to keep a local `error` state in the component.
---
### Summary
By making these adjustments, you'll have:
- All protected API calls using `fetchWithAuth`, which handles token refreshing and retries failed requests.
- Unauthorized users are redirected to the login page if they don't have valid tokens.
- On the server side, the `/api/cards` endpoint is protected with JWT, fulfilling Task 6.
- The frontend is properly handling authentication state, dispatching appropriate actions, and managing errors.
---
### Implementing All the Changes
Here's a checklist of what you need to do:
1. **Adjust `fetchWithAuth` in `jwtFunctions.js`**:
- Ensure it handles token refreshing and retries failed requests.
2. **Update Action Creators to Use `fetchWithAuth`**:
- Modify `fetchAllItems` and `fetchSearchItems`.
3. **Adjust `logoutUser` Function**:
- Clear tokens, dispatch `logoutAction`, and redirect.
4. **Implement Route Protection**:
- Use a `PrivateRoute` component or similar method.
5. **Update Reducers and Action Creators**:
- Ensure consistency in handling actions and state.
6. **Update `Login.jsx`**:
- Use Redux state for error handling.
7. **Test the Application Thoroughly**:
- Test login, token expiration, automatic token refreshing, and logout.
---
By following these steps and making the necessary code adjustments, you'll successfully implement the JWT authentication logic as per your tasks.
If you have any questions or encounter any issues while implementing these changes, feel free to ask for further clarification!