hoangvvo / nextjs-mongodb-app

A Next.js and MongoDB web application, designed with simplicity for learning and real-world applicability in mind.
https://nextjs-mongodb.now.sh/
MIT License
1.53k stars 286 forks source link

in edit profile, i am getting req.body is undefined #155

Open sc0rp10n-py opened 1 year ago

sc0rp10n-py commented 1 year ago

i added some values in the profile and want to edit them, so i did like in code here https://github.com/hoangvvo/nextjs-mongodb-app/blob/v2/pages/api/user/index.js

const {quote} = req.body;

but i get this error

Cannot destructure property 'baseName' of 'req.body' as it is undefined."

even console.log(req.body) gives undefined.

danielmeeusen commented 1 year ago

I need to see the code to be able to help you

sc0rp10n-py commented 1 year ago

So i was able to read the body using this buffer function I found in one of the issues on nextjs github issues

But i am having trouble with sending profile picture to backend to be uploaded or updated. this is the API code

import { ValidateProps } from '@/api-lib/constants';
import { findUserByEmail, updateUserById } from '@/api-lib/db';
import { auths, validateBody } from '@/api-lib/middlewares';
import { getMongoDb } from '@/api-lib/mongodb';
import { ncOpts } from '@/api-lib/nc';
// import { slugUsername } from '@/lib/user';
import { v2 as cloudinary } from 'cloudinary';
import multer from 'multer';
import nc from 'next-connect';
// import { Readable } from 'node:stream';

const upload = multer({ dest: '/tmp' });
const handler = nc(ncOpts);

if (process.env.CLOUDINARY_URL) {
  const {
    hostname: cloud_name,
    username: api_key,
    password: api_secret,
  } = new URL(process.env.CLOUDINARY_URL);

  cloudinary.config({
    cloud_name,
    api_key,
    api_secret,
  });
}

handler.use(...auths);

handler.get(async (req, res) => {
  if (!req.user) return res.json({ user: null });
  return res.json({ user: req.user });
});

async function buffer(readable) {
  const chunks = [];
  for await (const chunk of readable) {
    chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
  }
  return Buffer.concat(chunks);
}

handler.patch(
  upload.single('profilePicture'),
  validateBody({
    type: 'object',
    properties: {
      username: ValidateProps.user.username,
    },
    additionalProperties: true,
  }),
  async (req, res) => {
    if (!req.user) {
      req.status(401).end();
      return;
    }

    const db = await getMongoDb();

    let profilePicture;
    if (req.file) {
      const image = await cloudinary.uploader.upload(req.file.path, {
        width: 512,
        height: 512,
        crop: 'fill',
      });
      profilePicture = image.secure_url;
    }

    const buf = await buffer(req);
    const rawBody = buf.toString('utf-8');
    const body = JSON.parse(rawBody);
    const { baseName, quote, email } = body;

    // let username;

    if (body.email) {
      // username = slugUsername(req.body.username);
      if (
        email !== body.email &&
        (await findUserByEmail(db, email))
      ) {
        res
          .status(403)
          .json({ error: { message: 'The email has already been taken.' } });
        return;
      }
    }

    const user = await updateUserById(db, req.user._id, {
      // ...(username && { username }),
      ...(profilePicture && { profilePicture }),
      ...(baseName && { baseName }),
      ...(quote && { quote }),
      ...(email && { email }),
    });

    res.status(200).json({ user });
  }
);

export const config = {
  api: {
    bodyParser: false,
  },
};

export default handler;

this is the frontend code

const Game = ({ logoutSound, clickSound }) => {
    const [username, setUsername] = useState("");
    const [email, setEmail] = useState("");
    const [verified, setVerified] = useState(false);
    const [oldPassword, setOldPassword] = useState("");
    const [newPassword, setNewPassword] = useState("");
    const [baseName, setBaseName] = useState("");
    const [quote, setQuote] = useState("");
    const [avatar, setAvatar] = useState("");
    const [avatarUrl, setAvatarUrl] = useState(null);
    const [quoteSize, setQuoteSize] = useState("");

    const router = useRouter();

    useEffect(() => {
        const loggedInUser = sessionStorage.getItem("loggedIn") || false;
        if (loggedInUser) {
            const userInfo = JSON.parse(sessionStorage.getItem("user"));
            console.log(userInfo);
            setUsername(userInfo.user.username);
            setEmail(userInfo.user.email);
            setVerified(userInfo.user.emailVerified);
            setBaseName(userInfo.user.baseName);
            setQuote(userInfo.user.quote);
            setAvatarUrl(userInfo.user.profilePicture);
        } else {
            router.push("/login");
        }
    }, []);

    const updateProfile = async () => {
        // e.preventDefault();
        const formData = new FormData();
        formData.append("username", username);
        formData.append("email", email);
        formData.append("baseName", baseName);
        formData.append("quote", quote);
        formData.append("profilePicture", avatar);
        await fetch("/api/user", {
            method: "PATCH",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                username,
                email,
                baseName,
                quote,
                avatar,
            }),
        })
            .then(async (res) => {
                if (res.status === 200) {
                    const data = await res.json();
                    sessionStorage.setItem("user", JSON.stringify(data));
                } else {
                    toast.error("Error updating profile");
                }
            })
            .catch((err) => {
                toast.error("Error updating profile");
            });
    };

    const updatePassword = async () => {
        await fetch("/api/user/password", {
            method: "PUT",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                oldPassword,
                newPassword,
            }),
        })
            .then(async (res) => {
                if (res.status === 200) {
                    toast.success("Password updated");
                    await onLogout();
                } else {
                    toast.error("Error updating password");
                }
            })
            .catch((err) => {
                toast.error("Error updating password");
            });
    };

    return (
        <>
            <div>
                <label className="absolute top-[-5px] right-[-5px] bg-dark rounded-full p-[5px] cursor-pointer">
                    <CiEdit size={20} />
                    <input
                        type="file"
                        className="hidden"
                        onChange={(e) => {
                            const file = e.target.files[0];
                            const reader = new FileReader();
                            reader.onload = (l) => {
                                setAvatarUrl(l.target.result);
                            };
                            reader.readAsDataURL(file);
                            setAvatar(file);
                        }}
                    />
                </label>
                {avatarUrl ? (
                    <>
                        <Image
                            src={avatarUrl}
                            className="bg-dark w-20 h-20 rounded-lg"
                            alt={username}
                            width={80}
                            height={80}
                        />
                    </>
                ) : (
                    <>
                        <div className="bg_drop bg-dark w-20 h-20 rounded-lg"></div>
                    </>
                )}
            </div>
            <div className="mb-5 w-3/4">
                <input
                    type="text"
                    className="border rounded-lg px-4 py-2 outline-none text-2xl bg-dark font-bold text-center w-full"
                    placeholder="Your Base Name"
                    //baseName's Base
                    value={baseName}
                    onChange={(e) => {
                        setBaseName(e.target.value);
                    }}
                />
            </div>
            <textarea
                className="border w-3/4 outline-none p-4 bg-dark text-xl rounded-lg mb-5"
                placeholder="Enter your quote here..."
                value={quote}
                onChange={(e) => {
                    setQuote(e.target.value);
                }}
            ></textarea>
            <div className="border p-4 w-3/4 rounded-lg mb-5 bg-dark">
                <div className="mb-5">
                    <label className="text-lg">Username</label>
                    <input
                        type="text"
                        className="block border-b outline-none text-xl bg-transparent"
                        required
                        placeholder="Username"
                        value={username}
                        readonly
                    />
                </div>
                <div className="mb-5">
                    <label className="text-lg">Email</label>
                    <input
                        type="email"
                        className="block border-b outline-none text-xl bg-transparent"
                        required
                        placeholder="Email"
                        value={email}
                        onChange={(e) => {
                            setEmail(e.target.value);
                        }}
                    />
                </div>
            </div>
            <button
                draggable="false"
                className="bg_drop bg-black border rounded-lg pt-2 pb-3 px-14 text-2xl font-semibold mb-4 transition-transform hover:scale-95"
                onClick={updateProfile}
            >
                Save
            </button>
            <div className="border p-4 w-3/4 rounded-lg mb-5 bg-dark">
                <div className="mb-5">
                    <label className="text-lg">Old Password</label>
                    <input
                        type="password"
                        className="block border-b outline-none text-xl bg-transparent"
                        required
                        placeholder="Old Password"
                        value={oldPassword}
                        onChange={(e) => {
                            setOldPassword(e.target.value);
                        }}
                    />
                </div>
                <div className="">
                    <label className="text-lg">New Password</label>
                    <input
                        type="password"
                        className="block border-b outline-none text-xl bg-transparent"
                        required
                        placeholder="New Password"
                        value={newPassword}
                        onChange={(e) => {
                            setNewPassword(e.target.value);
                        }}
                    />
                </div>
            </div>
            <button
                draggable="false"
                className="bg_drop bg-black border rounded-lg pt-2 pb-3 px-14 text-xl font-semibold transition-transform hover:scale-95"
                onClick={updatePassword}
            >
                Update Password
            </button>
        </>
    );
};
export default Game;
danielmeeusen commented 1 year ago

What are you getting on your api end? Why are you not using any ref's like in the example? Also I don't think you need to stringify formData.

sc0rp10n-py commented 1 year ago

What are you getting on your api end? Why are you not using any ref's like in the example? Also I don't think you need to stringify formData.

I needed useStates instead of useRef The API sends empty array {} for profilePicture

Does using useRef instead of useState makes much of a difference in this case?

danielmeeusen commented 1 year ago

It does, refs and state are completely different things. Personally I would try to build your form the way it is laid out in the example before changing it.

sc0rp10n-py commented 1 year ago

if do this API call

const updateProfile = async () => {
    // e.preventDefault();
    setLoading(true);
    await fetch("/api/user", {
        method: "PATCH",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            username,
            email,
            baseName,
            quote,
            profilePicture,
        }),
    })
        .then(async (res) => {
            if (res.status === 200) {
                const data = await res.json();
                sessionStorage.setItem("user", JSON.stringify(data));
                toast.success("Profile updated");
                setLoading(false);
            } else {
                toast.error("Error updating profile");
                setLoading(false);
            }
        })
        .catch((err) => {
            toast.error("Error updating profile");
            setLoading(false);
        });
};

then this is the response i get from backend server

{"error":{"message":"\"\" must be object"}}

sc0rp10n-py commented 1 year ago

ok i fixed that by removing this

// validateBody({
  //   type: 'object',
  //   properties: {
  //     username: ValidateProps.user.username,
  //   },
  //   additionalProperties: true,
  // }),

i forgot to remove it earlier.

only issue is that profile doesn't go as expected

ok let me try using refs

sc0rp10n-py commented 1 year ago

@danielmeeusen hi so i used ref but still profilepicture is going as empty only

my code

const Game = ({ logoutSound, clickSound }) => {
    const [username, setUsername] = useState("");
    const [email, setEmail] = useState("");
    const [verified, setVerified] = useState(false);
    const [oldPassword, setOldPassword] = useState("");
    const [newPassword, setNewPassword] = useState("");
    const [baseName, setBaseName] = useState("");
    const [quote, setQuote] = useState("");
    const profilePictureRef = useRef();
    const [avatarUrl, setAvatarUrl] = useState(null);
    const [quoteSize, setQuoteSize] = useState("");
    const [loading, setLoading] = useState(false);

    const updateProfile = async () => {
        // e.preventDefault();
        setLoading(true);
        const profilePicture = profilePictureRef.current.files[0];
        await fetch("/api/user", {
            method: "PATCH",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                username,
                email,
                profilePicture,
                quote,
                baseName,
            }),
        })
            .then(async (res) => {
                if (res.status === 200) {
                    const data = await res.json();
                    sessionStorage.setItem("user", JSON.stringify(data));
                    toast.success("Profile updated");
                    setLoading(false);
                } else {
                    toast.error("Error updating profile");
                    setLoading(false);
                }
            })
            .catch((err) => {
                toast.error("Error updating profile");
                setLoading(false);
            });
    };

    return (
        <>
            <div className="z-[9999] fixed top-0 right-0 w-full h-screen bg-gray-500/50 flex justify-center items-center">
                <div className="container mx-auto my-5 h-[90%]">
                    <div className="bg-black bg-[url('/images/background.png')] bg-cover bg-center rounded-lg p-10 h-full overflow-auto flex flex-col scrollable scrollable-light">
                        <div className="flex justify-end">
                            <button
                                draggable="false"
                                className="transition-transform hover:scale-95 mr-5"
                                onClick={() => {
                                    setModalProfile(false);
                                }}
                            >
                                <ImCross className="" />
                            </button>
                        </div>
                        <div className="flex flex-row flex-wrap items-center my-auto">
                            <div className="md:basis-1/3 shrink-0 px-4 flex flex-col items-center justify-center border-r-2">
                                <div className="relative mb-5">
                                    <label className="absolute top-[-5px] right-[-5px] bg-dark rounded-full p-[5px] cursor-pointer">
                                        <CiEdit size={20} />
                                        <input
                                            type="file"
                                            accept="image/*"
                                            ref={profilePictureRef}
                                            className="hidden"
                                            onChange={(e) => {
                                                const file =
                                                    e.currentTarget.files[0];
                                                if (!file) return;
                                                const reader = new FileReader();
                                                reader.onload = (l) => {
                                                    setAvatarUrl(
                                                        l.currentTarget.result
                                                    );
                                                };
                                                reader.readAsDataURL(file);
                                            }}
                                        />
                                    </label>
                                    {avatarUrl ? (
                                        <>
                                            <Image
                                                src={avatarUrl}
                                                className="bg-dark w-20 h-20 rounded-lg"
                                                alt={username}
                                                width={80}
                                                height={80}
                                            />
                                        </>
                                    ) : (
                                        <>
                                            <div className="bg_drop bg-dark w-20 h-20 rounded-lg"></div>
                                        </>
                                    )}
                                </div>
                                <div className="mb-5 w-3/4">
                                    <input
                                        type="text"
                                        className="border rounded-lg px-4 py-2 outline-none text-2xl bg-dark font-bold text-center w-full"
                                        placeholder="Your Base Name"
                                        //baseName's Base
                                        value={baseName}
                                        onChange={(e) => {
                                            setBaseName(e.target.value);
                                        }}
                                    />
                                </div>
                                <textarea
                                    className="border w-3/4 outline-none p-4 bg-dark text-xl rounded-lg mb-5"
                                    placeholder="Enter your quote here..."
                                    value={quote}
                                    onChange={(e) => {
                                        setQuote(e.target.value);
                                    }}
                                ></textarea>
                                <div className="border p-4 w-3/4 rounded-lg mb-5 bg-dark">
                                    <div className="mb-5">
                                        <label className="text-lg">
                                            Username
                                        </label>
                                        <input
                                            type="text"
                                            className="block border-b outline-none text-xl bg-transparent"
                                            required
                                            placeholder="Username"
                                            value={username}
                                            readonly
                                        />
                                    </div>
                                    <div className="mb-5">
                                        <label className="text-lg">Email</label>
                                        <input
                                            type="email"
                                            className="block border-b outline-none text-xl bg-transparent"
                                            required
                                            placeholder="Email"
                                            value={email}
                                            onChange={(e) => {
                                                setEmail(e.target.value);
                                            }}
                                        />
                                    </div>
                                </div>
                                <button
                                    draggable="false"
                                    className="bg_drop bg-black border rounded-lg pt-2 pb-3 px-14 text-2xl font-semibold mb-4 transition-transform hover:scale-95"
                                    onClick={updateProfile}
                                >
                                    Save
                                    {/* {loading ? <Loading /> : "Save"} */}
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </>
    );
};

API code

import { ValidateProps } from '@/api-lib/constants';
import { findUserByEmail, updateUserById } from '@/api-lib/db';
import { auths, validateBody } from '@/api-lib/middlewares';
import { getMongoDb } from '@/api-lib/mongodb';
import { ncOpts } from '@/api-lib/nc';
// import { slugUsername } from '@/lib/user';
import { v2 as cloudinary } from 'cloudinary';
import multer from 'multer';
import nc from 'next-connect';
// import { Readable } from 'node:stream';

const upload = multer({ dest: '/tmp' });
const handler = nc(ncOpts);

if (process.env.CLOUDINARY_URL) {
  const {
    hostname: cloud_name,
    username: api_key,
    password: api_secret,
  } = new URL(process.env.CLOUDINARY_URL);

  cloudinary.config({
    cloud_name,
    api_key,
    api_secret,
  });
}

handler.use(...auths);

handler.get(async (req, res) => {
  if (!req.user) return res.json({ user: null });
  return res.json({ user: req.user });
});

async function buffer(readable) {
  const chunks = [];
  for await (const chunk of readable) {
    chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
  }
  return Buffer.concat(chunks);
}

handler.patch(
  upload.single('profilePicture'),
  // validateBody({
  //   type: 'object',
  //   properties: {
  //     username: ValidateProps.user.username,
  //   },
  //   additionalProperties: true,
  // }),
  async (req, res) => {
    if (!req.user) {
      req.status(401).end();
      return;
    }

    const db = await getMongoDb();

    let profilePicture;
    if (req.file) {
      const image = await cloudinary.uploader.upload(req.file.path, {
        width: 512,
        height: 512,
        crop: 'fill',
      });
      profilePicture = image.secure_url;
    }

    const buf = await buffer(req);
    const rawBody = buf.toString('utf-8');
    const body = JSON.parse(rawBody);
    const { baseName, quote, email } = body;

    // let username;

    if (body.email) {
      // username = slugUsername(req.body.username);
      if (
        email !== body.email &&
        (await findUserByEmail(db, email))
      ) {
        res
          .status(403)
          .json({ error: { message: 'The email has already been taken.' } });
        return;
      }
    }

    const user = await updateUserById(db, req.user._id, {
      // ...(username && { username }),
      ...(profilePicture && { profilePicture }),
      ...(baseName && { baseName }),
      ...(quote && { quote }),
      ...(email && { email }),
    });

    res.status(200).json({ user });
  }
);

export const config = {
  api: {
    bodyParser: false,
  },
};

export default handler;

network tab pic image

danielmeeusen commented 1 year ago

I guess you don't need a ref if you want to do it that way but you sill still need to use a formData interface though.

Here is an example: https://codesandbox.io/s/thyb0?file=/pages/index.js:523-725

  const updateProfile= async (e) => {
    const body = new FormData();

    body.append("file", avatarUrl);
    body.append("username", username);
    // etc...
    const res = await fetch("/api/user", {
      method: "PATCH",
      body
    });
  };