DEV Community

Cover image for Image Management and Database Integration with Node.js, Multer, Cloudinary and MySQL
Nuel
Nuel

Posted on

Image Management and Database Integration with Node.js, Multer, Cloudinary and MySQL

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
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

Project structure:

project-structure

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:

npm i multer cloudinary
Enter fullscreen mode Exit fullscreen mode

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.

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 });
  }
};
Enter fullscreen mode Exit fullscreen mode

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:

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 };
Enter fullscreen mode Exit fullscreen mode

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:

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);
  },
});
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

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:

.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
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

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.

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,
};
Enter fullscreen mode Exit fullscreen mode

Step 9: Create profileController function

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,
};
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

This route ties everything together, ensuring that only authenticated users can access the profile editing feature.

Step 11: Server Configuration

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}`);
});
Enter fullscreen mode Exit fullscreen mode

With this configuration, users can now access and update their profiles via the /update_profile route.

Find the project Github repo here. 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 😉

Top comments (0)