diff --git a/client/src/components/Anonymous.jsx b/client/src/components/Anonymous.jsx index 539aa80d..09e5e2c1 100644 --- a/client/src/components/Anonymous.jsx +++ b/client/src/components/Anonymous.jsx @@ -28,6 +28,8 @@ import { createClassesFromArray, isExplicitDisconnection } from 'src/lib/utils'; import useKeyPress, { ShortcutFlags } from 'src/hooks/useKeyPress'; import useCheckTimePassed from 'src/hooks/useCheckTimePassed'; +import { useAuth } from 'src/context/AuthContext'; +import { api } from 'src/lib/axios'; const centerItems = `flex items-center justify-center`; @@ -36,6 +38,7 @@ const Anonymous = ({ }) => { const { app, endSearch } = useApp(); + const { authState } = useAuth() const { currentChatId, onlineStatus } = app; const { clearTimer } = useCheckTimePassed(); @@ -54,7 +57,7 @@ const Anonymous = ({ const typingStatusTimeoutRef = useRef(null); const navigate = useNavigate(); - const { closeChat } = useChat(); + const { messages: state, closeChat } = useChat(); const { setDialog } = useDialog(); const onDisplay = useCallback(({ isTyping, chatId }) => { @@ -170,6 +173,59 @@ const Anonymous = ({ }); }; + const blockUser = async () => { + // Get the other user id + const chattingPartnersId = state[currentChatId]?.userIds.find( + id => id !== authState.loginId && id !== authState.email + ); + + if (!chattingPartnersId) { + return { success: false, message: "could not find user to block" }; + } + + try { + const res = await api.post('/blockUser', { + userIdToBlock: chattingPartnersId, + currentUserId: authState.loginId + }); + + if (res.status === 200) { + return { success: true }; + } else { + return { success: false, message: "Error reporting user" }; + } + } catch (error) { + console.error("Error in reportUser:", error); + return { success: false, message: "An unexpected error occurred" }; + } + } + + const handleBlock = async () => { + // Check if user have an account i.e. not a anonymous user + if(authState.loginType === "anonymous") { + setDialog({ + isOpen: true, + text: "You have to create an account first to access this feature!", + yesBtnText: "Create an account", + noBtnText: "Back to chat", + handler: () => navigate("/profile") + }) + return + } + + try { + const result = await blockUser(); + if (result.success) { + alert('User blocked successfully'); + closeChatHandler(false) + } else { + alert(result.message || "Error blocking user. Please try again later."); + } + } catch (err) { + console.error("Error in handleBlock:", err); + } + } + useKeyPress(['x'], () => handleClose(), ShortcutFlags.ctrl | ShortcutFlags.shift); useKeyPress(['n'], () => handleClose(true), ShortcutFlags.ctrl | ShortcutFlags.alt); @@ -279,6 +335,11 @@ const Anonymous = ({ Ctrl + Alt + N + handleBlock()} className="sm:w-[200px]"> +
+ Block User +
+
{ ); }; -export default Chat; +export default Chat; \ No newline at end of file diff --git a/server/controllers/userController.js b/server/controllers/userController.js index 6e5164af..d29bc640 100644 --- a/server/controllers/userController.js +++ b/server/controllers/userController.js @@ -11,11 +11,13 @@ const imageUpload = multer({ storage: storage }); const User = require('../models/UserModel'); const { emailValidator, generateObjectId } = require('../utils/helper'); +const { isUserBlocked, blockUser } = require('../utils/lib.js'); const { OK, NOT_FOUND, INTERNAL_SERVER_ERROR, CONFLICT, + BAD_REQUEST, } = require('../httpStatusCodes.js'); const createUserWithAutoId = async (email) => { @@ -146,6 +148,27 @@ const deleteUser = async (req, res) => { } }; +const blockUserHandler = async (req, res) => { + const {userIdToBlock, currentUserId} = req.body + + try { + // Check if the user is already blocked + if (await isUserBlocked([userIdToBlock, currentUserId])) { + return res.status(BAD_REQUEST).json({ message: "This user is already blocked." }); + } + + // Block the user + await blockUser(userIdToBlock, currentUserId) + + res.status(OK).json({ message: "User blocked successfully" }); + } catch (error) { + console.error(error); + return res + .status(INTERNAL_SERVER_ERROR) + .json({ error: 'Internal server error' }); + } +} + UserRouter.route('/login').post(emailValidator, loginUser); UserRouter.route('/profile').post( imageUpload.single('profileImage'), @@ -155,4 +178,6 @@ UserRouter.route('/profile').post( UserRouter.route('/profile/:email').get(getProfile); UserRouter.route('/deleteUser').delete(emailValidator, deleteUser); //Email validation applied to the required request handlers +UserRouter.route("/blockUser").post(blockUserHandler) + module.exports = UserRouter; diff --git a/server/models/UserModel.js b/server/models/UserModel.js index 0ba0e6db..7c3cdc64 100644 --- a/server/models/UserModel.js +++ b/server/models/UserModel.js @@ -37,6 +37,10 @@ const UserSchema = new Schema( type: Schema.Types.ObjectId, ref: 'Chat', }, + blockedUsers: { + type: [String], + default: [] + } }, { timestamps: true, @@ -63,6 +67,7 @@ const UserSchema = new Schema( socketIds: [], currentChatId: this.currentChat?._id?.toString() || null, chatIds: [], + blockedUsers: [] }; }, }, diff --git a/server/sockets/join.js b/server/sockets/join.js index 66b77fad..b3f75b09 100644 --- a/server/sockets/join.js +++ b/server/sockets/join.js @@ -11,6 +11,7 @@ const { getRandomPairFromWaitingList, createChat, getActiveUser, + isUserBlocked, } = require('../utils/lib'); /** @@ -19,16 +20,24 @@ const { * * @param {Server} io */ + const matchMaker = async (io) => { while (getWaitingUserLen() > 1) { - const chat = await createChat(getRandomPairFromWaitingList()); + const users = getRandomPairFromWaitingList(); + + // Check if either user is blocked + if ( await isUserBlocked(users) ) { + continue + } + const chat = await createChat(users); io.to(chat.id).emit(NEW_EVENT_JOINED, { roomId: chat.id, userIds: chat.userIds, }); } }; + module.exports = (io, socket) => { socket.on(NEW_EVENT_JOIN, ({ loginId, email }) => { /** diff --git a/server/utils/lib.js b/server/utils/lib.js index 328a5eea..916ad741 100644 --- a/server/utils/lib.js +++ b/server/utils/lib.js @@ -5,6 +5,7 @@ const { ObjectId } = require('mongodb'); const ActiveUser = require('../models/UserModel'); const Chat = require('../models/ChatModel'); +const User = require('../models/UserModel'); const Message = require('../models/MessageModel'); const { generateObjectId } = require('./helper'); @@ -502,6 +503,38 @@ function getWaitingUserLen() { return Object.keys(waitingUsers).length; } +async function blockUser(userIdToBlock, currentUserId) { + try { + await User.findOneAndUpdate({ loginId: currentUserId }, { + $addToSet: { blockedUsers: userIdToBlock } + }); + } catch (error) { + console.log(`error blocking user: ${error}`); + } +} + +async function isUserBlocked(users) { + const [userOne, userTwo] = users; + + try { + const [userOneData, userTwoData] = await Promise.all([ + User.findOne({ loginId: userOne.loginId }), + User.findOne({ loginId: userTwo.loginId }) + ]); + + // Using 'OR' because one of the users might be anonymously logged in, + // and in such cases, userData could be null. + if (userOneData || userTwoData) { + const userOneBlocked = userOneData?.blockedUsers.includes(userTwo.loginId); + const userTwoBlocked = userTwoData?.blockedUsers.includes(userOne.loginId); + return userOneBlocked || userTwoBlocked; + } + return false; + } catch (error) { + console.log(`error checking if users are blocked: ${error}`); + } +} + module.exports = { init, createChat, @@ -521,4 +554,6 @@ module.exports = { addToWaitingList, delActiveUser, seenMessage, + blockUser, + isUserBlocked };