Image Management and Database Integration with Node.js, Multer, Cloudinary and MySQL
Tags: node, mysql, javascript, webdev
Published: 2023-10-26 13:03:14

In our previous article, we laid the foundation for a secure authentication system, ensuring user data privacy and application security. Read the previous article [here](https://dev.to/nuelobeto/building-a-nodejs-authentication-system-with-mysql-a-step-by-step-guide-19l9)
Today, we're back with an exciting update, introducing image handling and profile editing features to empower our users make adjustments to their profiles. Let's explore the implementation of these new functionalities and how they enhance the overall user experience.

**Prerequisites**
[cloudinary account](https://cloudinary.com/)

**Project structure:**

![project-structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a9u2k2c56l71yqxur7u4.PNG)

**Step 1: Installation**

Before we dive into the implementation details, let's install two important packages that will play a crucial role in enabling user registration and profile editing:

```bash
npm i multer cloudinary
```
Multer is a middleware for handling file uploads, while Cloudinary is a cloud-based media management solution. We will utilize these tools to manage image uploads for user profiles.

**Step 2: Create user profile**
In the previous article, we implemented a register function which creates an account for a new user. We'll add a few more codes to this function which creates a profile for the user after the user is created.

```js
const { v4: uuidv4 } = require("uuid");
const jwt = require("jsonwebtoken");
const userSchema = require("../schemas/userSchema");
const bcrypt = require("bcryptjs");
const {
  createTable,
  checkRecordExists,
  insertRecord,
} = require("../utils/sqlFunctions");
const profileSchema = require("../schemas/profileSchema");

const register = async (req, res) => {
  const { email, password } = req.body;

  // Check for empty email or password fields
  if (!email || !password) {
    res
      .status(400)
      .json({ error: "Email or Password fields cannot be empty!" });
    return;
  }

  // Hash the user's password
  const salt = await bcrypt.genSalt(10);
  const hashedPassword = await bcrypt.hash(password, salt);

  // Create a user record
  const user = {
    userId: uuidv4(),
    email,
    password: hashedPassword,
  };

  // Create a profile record
  const profile = {
    profileId: uuidv4(),
    userId: user.userId,
    email: user.email,
  };

  try {
    // Create necessary tables
    await createTable(userSchema);
    await createTable(profileSchema);

    // Check if the user already exists
    const userAlreadyExists = await checkRecordExists("users", "email", email);

    if (userAlreadyExists) {
      res.status(409).json({ error: "Email already exists" });
    } else {
      await insertRecord("users", user);
      await insertRecord("profiles", profile);

      res.status(201).json({ message: "User created successfully!" });
    }
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};
```
**Step 3: Implement user Authentication Middleware**
Just like our previous article, user authentication remains a core component of our application's security. We've enhanced our authentication middleware to ensure that only authenticated users can access the profile editing feature.

Inside authMiddleware.js:

```js
const jwt = require("jsonwebtoken");
const { checkRecordExists } = require("../utils/sqlFunctions");

const requiresAuth = async (req, res, next) => {
  const authorizationHeader = req.headers.authorization;

  if (!authorizationHeader || !authorizationHeader.startsWith("Bearer")) {
    return res.status(401).json({ error: "Not authorized, no token" });
  }

  try {
    const token = authorizationHeader.split(" ")[1];

    if (!token) {
      return res.status(401).json({ error: "Not authorized, no token" });
    }

    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    if (!decoded || !decoded.userId) {
      return res.status(401).json({ error: "Invalid token" });
    }

    const user = await checkRecordExists("users", "userId", decoded.userId);

    if (!user) {
      return res.status(401).json({ error: "User not found" });
    }

    req.user = user;
    delete req.user.password;

    next();
  } catch (error) {
    console.error(error);
    return res.status(401).json({ error: "Token verification failed" });
  }
};

module.exports = { requiresAuth };
```
This middleware is used to protect routes that require authentication. It ensures that only users with a valid, unexpired token and a corresponding user record in the database can access the protected resources. If the user is authenticated, their information is made available in the req object for subsequent route handlers to use. If not authenticated or any verification fails, the middleware sends an error response.

**Step 4: Multer Configuration**
To handle image uploads in our profile editing feature, we've set up a Multer configuration in the utils/multer.js file. This configuration ensures that only image files with specific extensions (jpg, jpeg, and png) are accepted:

```js
const multer = require("multer");
const path = require("path");

module.exports = multer({
  storage: multer.diskStorage({}),
  fileFilter: (req, file, cb) => {
    let ext = path.extname(file.originalname);
    if (ext !== ".jpg" && ext !== ".jpeg" && ext !== ".png") {
      cb(new Error("Unsupported file type!"), false);
      return;
    }
    cb(null, true);
  },
});
```
**Step 5: Cloudinary Integration**
To store and manage user profile images, we've integrated Cloudinary. In the utils/cloudinary.js file, we configure Cloudinary with the necessary credentials:

```js
const cloudinary = require("cloudinary").v2;

cloudinary.config({
  cloud_name: process.env.CLOUD_NAME,
  api_key: process.env.CLOUD_API_KEY,
  api_secret: process.env.CLOUD_API_SECRET,
});

module.exports = cloudinary;
```
**Step 6: Update environment variables**
We're going to need some extra variables for cloudinary to work properly. We'll update our .env file with the necessary variables: 

```js
.env: PORT=5000
JWT_SECRET=YOUR_JWT_SECRET
HOST=localhost
USER=root
PASSWORD=""
DATABASE=profilePro
CLOUD_NAME=YOUR_CLOUD_NAME
CLOUD_API_KEY=YOUR_CLOUD_API_KEY
CLOUD_API_SECRET=YOUR_CLOUD_API_SECRET
```
CLOUD_NAME, CLOUD_API_KEY, CLOUD_API_SECRET can be obtained from your cloudinary account.

**Step 7: Defining the Profile schema**
In schemas/profileSchema.js:

```js
const profileSchema = `
  CREATE TABLE IF NOT EXISTS profiles (
    profileId VARCHAR(255) UNIQUE NOT NULL,
    userId VARCHAR(255) UNIQUE NOT NULL,
    image VARCHAR(255),
    name VARCHAR(255),
    email VARCHAR(255) NOT NULL,
    phone VARCHAR(255),
    address VARCHAR(255),
    country VARCHAR(255),
    state VARCHAR(255),
    city VARCHAR(255),
    zip VARCHAR(255),
    about VARCHAR(255),
    links VARCHAR(255)
  )
`;

module.exports = profileSchema;
```
**Step 8: Update SQL functions**

In order to allow users update their profiles, we'll add an update function to the SQL functions we already have.

```js
const updateRecord = (tableName, updates, column, value) => {
  return new Promise((resolve, reject) => {
    const columnValues = Object.keys(updates)
      .map((column) => `${column} = ?`)
      .join(", ");
    const query = `UPDATE ${tableName} SET ${columnValues} WHERE ${column} = ?`;

    pool.query(query, Object.values(updates).concat(value), (err, results) => {
      if (err) {
        reject(err);
      } else {
        resolve(results);
      }
    });
  });
};

module.exports = {
  createTable,
  checkRecordExists,
  insertRecord,
  updateRecord,
};
```

**Step 9: Create profileController function**

```js
const { checkRecordExists, updateRecord } = require("../utils/sqlFunctions");
const cloudinary = require("../utils/cloudinary");

const updateProfile = async (req, res) => {
  try {
    const profile = await checkRecordExists(
      "profiles",
      "userId",
      req.user.userId
    );

    const updates = {
      ...req.body,
    };

    if (req.file) {
      if (!profile.image) {
        const image = await cloudinary.uploader.upload(req.file.path, {
          folder: "profilePro",
        });
        updates.image = image.secure_url;
      } else {
        const image_url = profile.image.split("/");
        const publicId = image_url[image_url.length - 1].split(".")[0];
        await cloudinary.uploader.destroy(`profilePro/${publicId}`);

        const image = await cloudinary.uploader.upload(req.file.path, {
          folder: "profilePro",
        });
        updates.image = image.secure_url;
      }
    }

    await updateRecord("profiles", updates, "profileId", profile.profileId);
    res.json({ message: "Profile Updated Successfully" });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

module.exports = {
  updateProfile,
};
```
In the code above, we're getting the user's profile using the user's information which we have saved in the req from the auth middleware, then we set the fields we want to update, upload the profile image to cloudinary and set the image to the image url from cloudinary, finally we update the user's profile using the updateRecord SQL function.

**Step 10: Create profileRoutes**

```js
const express = require("express");
const router = express.Router();
const upload = require("../utils/multer");

const { updateProfile } = require("../controllers/profileControllers");
const { requiresAuth } = require("../middlewares/authMiddleware");

router.put("/update_profile", requiresAuth, upload.single("image"), updateProfile);

module.exports = router;
```
This route ties everything together, ensuring that only authenticated users can access the profile editing feature.

**Step 11: Server Configuration**

```js
const express = require("express");
const dotenv = require("dotenv");
dotenv.config();
const cors = require("cors");
const connectDB = require("./db/db");
const port = process.env.PORT;
const authRoutes = require("./routes/authRoutes");
const profileRoutes = require("./routes/profileRoutes");

const app = express();

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/", authRoutes, profileRoutes); // Include the profile routes

connectDB();

app.listen(port, () => {
  console.log(`Server running on port: ${port}`);
});
```
With this configuration, users can now access and update their profiles via the /update_profile route.

Find the project Github repo [here](https://github.com/nuelobeto/profilePro). Happy coding! 🎉

**Conclusion**

With the addition of the profile editing feature, ProfilePro has evolved into a more versatile and user-friendly application. Users can now customize their profiles, upload images, and keep their information up-to-date. We've utilized powerful tools like Multer and Cloudinary to manage image uploads and seamlessly integrated them into our authentication and database systems.

In the next article, we would build a beautiful user interface with React enabling our users to register, login, edit their profiles, upload profile image and logout. Stay tuned 😉