diff --git a/backend/user/controller/user-controller.js b/backend/user/controller/user-controller.js index 01b3d24312..8008244619 100644 --- a/backend/user/controller/user-controller.js +++ b/backend/user/controller/user-controller.js @@ -52,11 +52,18 @@ export async function createUser(req, res) { try { const { username, email, password } = req.body; if (username && email && password) { - const existingUser = await _findUserByUsernameOrEmail(username, email); - if (existingUser) { + const existingUsername = await _findUserByUsername(username); + if (existingUsername) { return res .status(409) - .json({ message: "username or email already exists" }); + .json({ message: "username already exists" }); + } + + const existingEmail = await _findUserByEmail(email); + if (existingEmail) { + return res + .status(409) + .json({ message: "email already exists" }); } const salt = bcrypt.genSaltSync(10); @@ -166,6 +173,11 @@ export async function updateUser(req, res) { hashedPassword = bcrypt.hashSync(password, salt); } + let isVerified = user.isVerified; + if (email && email !== user.email) { + isVerified = false; + } + const updatedUser = await _updateUserById( userId, username, @@ -174,7 +186,9 @@ export async function updateUser(req, res) { bio, linkedin, github, - profilePictureUrl + profilePictureUrl, + isVerified, + user.verificationCode, ); return res.status(200).json({ message: `Updated data for user ${userId}`, @@ -183,7 +197,7 @@ export async function updateUser(req, res) { } else { return res.status(400).json({ message: - "No field to update: username and email and password are all missing!", + "No field to update!", }); } } catch (err) { @@ -251,7 +265,54 @@ export async function deleteUser(req, res) { } } -// send email to user +export async function requestVerificationEmail(req, res) { + try { + const { email } = req.body; + if (email) { + const user = await _findUserByEmail(email); + if (!user) { + return res + .status(404) + .json({ message: `User not found with email ${email}` }); + } + + // For email verification, you can generate a random verification code and save it in the database + const verificationCode = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + + const updatedUser = await _updateUserById( + user.id, + user.username, + user.email, + user.password, + user.bio, + user.linkedin, + user.github, + user.profilePictureUrl, + user.isVerified, + verificationCode + ); + + // Send email to user + await sendVerificationEmail(email, user.username, verificationCode); + + return res + .status(200) + .json({ + message: `Sent verification email to user ${user.username}`, + }); + } else { + return res.status(400).json({ message: "email is missing!" }); + } + } catch (err) { + console.error(err); + return res + .status(500) + .json({ message: "Unknown error when requesting verification email!" }); + } +} + export async function verifyUser(req, res) { try { const { code } = req.query; @@ -367,7 +428,10 @@ export async function resetPasswordUsingCode(req, res) { hashedPassword, user.bio, user.linkedin, - user.github + user.github, + user.profilePictureUrl, + user.isVerified, + user.verificationCode ); return res.status(200).json({ message: `Reset password for user ${user.username}`, diff --git a/backend/user/model/repository.js b/backend/user/model/repository.js index 0d1b69caf3..6bebcf3fb8 100644 --- a/backend/user/model/repository.js +++ b/backend/user/model/repository.js @@ -53,7 +53,9 @@ export async function updateUserById( bio, linkedin, github, - profilePictureUrl + profilePictureUrl, + isVerified, + verificationCode ) { return UserModel.findByIdAndUpdate( userId, @@ -66,6 +68,8 @@ export async function updateUserById( linkedin, github, profilePictureUrl, + isVerified, + verificationCode }, }, { new: true } // return the updated user diff --git a/backend/user/routes/user-routes.js b/backend/user/routes/user-routes.js index 837bec6977..e461f83b8a 100644 --- a/backend/user/routes/user-routes.js +++ b/backend/user/routes/user-routes.js @@ -11,6 +11,7 @@ import { updateUser, updateUserPrivilege, getFileUrl, + requestVerificationEmail, verifyUser, } from "../controller/user-controller.js"; import { @@ -30,6 +31,8 @@ router.patch( updateUserPrivilege ); +router.post("/request-verification-email", requestVerificationEmail); + router.get("/verify", verifyUser); router.post("/request-password-reset", requestPasswordReset); diff --git a/frontend/src/api/user.tsx b/frontend/src/api/user.tsx index 3715b43468..3b3b46cfc1 100644 --- a/frontend/src/api/user.tsx +++ b/frontend/src/api/user.tsx @@ -330,3 +330,44 @@ export const resetPasswordWithCode = async (code: string, password: string) => { return true; }; + +export const requestVerificationEmail = async (email: string) => { + if (!email) { + toast.fire({ + icon: "error", + title: "Please provide an email", + }); + return false; + } + + toast.fire({ + icon: "info", + title: "Requesting verification email...", + }); + const response = await fetch( + `${NEXT_PUBLIC_IAM_USER_SERVICE}/request-verification-email`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email }), + } + ); + const data = await response.json(); + + if (response.status !== 200) { + toast.fire({ + icon: "error", + title: data.message, + }); + return false; + } + + toast.fire({ + icon: "success", + title: "Verification email sent. Please check your email.", + }); + + return true; +} \ No newline at end of file diff --git a/frontend/src/app/(user)/user/[id]/page.tsx b/frontend/src/app/(user)/user/[id]/page.tsx index 6d1c0c6236..ebb5efaa0d 100644 --- a/frontend/src/app/(user)/user/[id]/page.tsx +++ b/frontend/src/app/(user)/user/[id]/page.tsx @@ -29,26 +29,38 @@ const ProfilePage = () => { }; return ( -
+

{user?.username}'s Profile!

-

Profile Age

- {calculateAge(user?.createdAt)} days + Profile Picture -

Email

-
-

{user?.email}

- +
+

PROFILE AGE

+ {calculateAge(user?.createdAt)} days
-

Bio

- {user?.bio && {user?.bio}} +
+

EMAIL

+
+

{user?.email}

+ +
+
-

GitHub

- {user?.github && {user?.github}} +
+

BIO

+ {user?.bio && {user?.bio}} +
-

LinkedIn

- {user?.linkedin && {user?.linkedin}} +
+

GITHUB URL

+ {user?.github && {user?.github}} +
+ +
+

LINKEDIN URL

+ {user?.linkedin && {user?.linkedin}} +
); diff --git a/frontend/src/app/(user)/user/me/page.tsx b/frontend/src/app/(user)/user/me/page.tsx index 3072209ce1..2113968acb 100644 --- a/frontend/src/app/(user)/user/me/page.tsx +++ b/frontend/src/app/(user)/user/me/page.tsx @@ -18,6 +18,7 @@ import { getUserId, updateUser, getFileUrl, + requestVerificationEmail } from "@/api/user"; import { useEffect, useRef, useState } from "react"; import { User } from "@/types/user"; @@ -131,6 +132,18 @@ const ProfilePage = () => { } }; + // must not trigger form submission + const onVerify = async (event: React.MouseEvent) => { + event.preventDefault(); + const res = await requestVerificationEmail(form.getValues("email") || ""); + if (res) { + Swal.fire({ + icon: "success", + title: "Verification email sent!", + }); + } + } + return ( !!token && (
@@ -216,7 +229,7 @@ const ProfilePage = () => { render={({ field }) => ( - EMAIL + EMAIL ({user?.isVerified ? "VERIFIED" : })