Open sc0rp10n-py opened 1 year ago
I need to see the code to be able to help you
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;
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.
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?
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.
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"}}
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
@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
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
});
};
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
but i get this error
even
console.log(req.body)
gives undefined.