diff --git a/backend/src/chat/chat.controller.ts b/backend/src/chat/chat.controller.ts index 93ef93fe..a0fc0fa5 100644 --- a/backend/src/chat/chat.controller.ts +++ b/backend/src/chat/chat.controller.ts @@ -6,11 +6,15 @@ import { HttpStatus, Logger, ParseIntPipe, + Patch, Post, Query, + Req, + UseGuards, } from '@nestjs/common'; import { ChatService } from './chat.service'; -import { chatMemberRole, chatType } from '@prisma/client'; +import { User, chatMemberRole, chatType } from '@prisma/client'; +import { AccessTokenGuard } from 'src/auth/jwt/jwt.guard'; // create a new chat in the database using post request // get all chats from the database using get request @@ -27,11 +31,13 @@ export class ChatController { ): Promise { const you = await this.chatService.getMemberFromChat(chatId, login); const member = await this.chatService.getMemberFromChat(chatId, user); + // Me and him must exist in the database if (!you || !member) { this.logger.error('Unable to find user or member'); return true; } + // I cannot be the member I want to mute, I cannot be member to mute, I cannot mute an admin nor the owner if (you === member || you.role === 'MEMBER' || member.role !== 'MEMBER') { this.logger.error('You are not allowed to mute this user'); @@ -189,4 +195,21 @@ export class ChatController { throw new HttpException({ error }, HttpStatus.FORBIDDEN); } } + + // Patch to update a chat password if the user is the owner + @UseGuards(AccessTokenGuard) + @Patch('/password') + async updateChatPassword( + @Req() request: Request & { user: User }, + @Body() body: { chatId: number; password: string }, + ) { + const { chatId, password } = body; + const { login } = request.user; + const updatedChat = await this.chatService.updateChatPassword( + chatId, + password, + login, + ); + return updatedChat; + } } diff --git a/backend/src/chat/chat.service.ts b/backend/src/chat/chat.service.ts index 061df210..9fe69ab3 100644 --- a/backend/src/chat/chat.service.ts +++ b/backend/src/chat/chat.service.ts @@ -1,4 +1,9 @@ -import { Injectable } from '@nestjs/common'; +import { + HttpException, + Injectable, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; import { Chat, ChatMember, @@ -666,6 +671,68 @@ export class ChatService { return isPasswordValid; } + async updateChatPassword( + chatId: number, + password: string, + userLogin: string, + ): Promise { + // check if user is the owner of the chat + const chat = await this.prisma.chat.findUnique({ + where: { + id: chatId, + }, + }); + + if (!chat) { + throw new NotFoundException('Chat not found'); + } + + if (chat.owner !== userLogin) { + throw new UnauthorizedException('You are not the owner of this chat'); + } + + if (chat.chatType !== 'PROTECTED' || !chat.password) { + throw new HttpException( + { + status: 403, + error: 'Chat is not protected', + }, + 403, + ); + } + + // if password is empty, remove password and set chat type to public + if (!password) { + const updatedChat = await this.prisma.chat.update({ + where: { + id: chatId, + }, + data: { + password: null, + chatType: 'PUBLIC', + }, + }); + + return updatedChat; + } + + try { + const updatedChat = await this.prisma.chat.update({ + where: { + id: chatId, + }, + data: { + password: await argon2.hash(password), + }, + }); + + return updatedChat; + } catch (error) { + console.log(error); + return null; + } + } + async getChatByName(name: string): Promise { try { const chat = await this.prisma.chat.findFirst({ diff --git a/frontend/src/app/(private)/layout.tsx b/frontend/src/app/(private)/layout.tsx index 0a70bf8f..22ffef06 100644 --- a/frontend/src/app/(private)/layout.tsx +++ b/frontend/src/app/(private)/layout.tsx @@ -17,16 +17,13 @@ export default function RootPrivateLayout({ children: React.ReactNode; }) { return ( - - - - -
{children}
- -
-
- - - + <> + + +
{children}
+ +
+
+ ); } diff --git a/frontend/src/app/auth/layout.tsx b/frontend/src/app/auth/layout.tsx index 2e501e4f..4c3ea74f 100644 --- a/frontend/src/app/auth/layout.tsx +++ b/frontend/src/app/auth/layout.tsx @@ -12,10 +12,8 @@ export default function RootLoginPublicLayout({ children: React.ReactNode; }) { return ( - - - {children} - - + <> + {children} + ); } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index f0fcded5..e7c7c31e 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -14,7 +14,7 @@ export default function RootPublicLayout({ children: React.ReactNode; }) { return ( - + {children} ); diff --git a/frontend/src/app/login/layout.tsx b/frontend/src/app/login/layout.tsx index fce5f5e1..026182b9 100644 --- a/frontend/src/app/login/layout.tsx +++ b/frontend/src/app/login/layout.tsx @@ -10,9 +10,5 @@ export default function RootLoginPublicLayout({ }: { children: React.ReactNode; }) { - return ( - - {children} - - ); + return <>{children}; } diff --git a/frontend/src/components/Chat/ChangeChatPassword.tsx b/frontend/src/components/Chat/ChangeChatPassword.tsx new file mode 100644 index 00000000..661374ad --- /dev/null +++ b/frontend/src/components/Chat/ChangeChatPassword.tsx @@ -0,0 +1,205 @@ +import { AuthContext } from "@/contexts/AuthContext"; +import { api } from "@/services/apiClient"; +import { queryClient } from "@/services/queryClient"; +import { Dialog, Transition } from "@headlessui/react"; +import { Lock } from "@phosphor-icons/react"; +import { Fragment, useContext, useState } from "react"; +import { useForm } from "react-hook-form"; +import toast from "react-hot-toast"; + +type ChangeChatPasswordProps = { + chatId: number; + handleHideLock: () => void; +}; + +export default function ChangeChatPassword({ + chatId, + handleHideLock, +}: ChangeChatPasswordProps) { + const [isOpen, setIsOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + + function closeModal() { + setIsOpen(false); + } + + function openModal() { + setIsOpen(true); + } + + const onSubmit = async (data: any) => { + // create a new form data and append the data, send to api + if (data.password) { + setIsLoading(true); + await api + .patch("/chat/password", { + chatId, + password: data.password, + }) + .then(() => { + toast.success("Senha atualizada!"); + }) + .catch((error) => { + toast.error( + `Erro ao atualizar senha: ${error.response.data.message}` + ); + }) + .finally(() => { + setIsLoading(false); + setIsOpen(false); + }); + } + }; + + const handleRemovePassword = async () => { + if (window.confirm("Tem certeza que deseja remover a senha do chat?")) { + // Handle password removal here + setIsLoading(true); + await api + .patch("/chat/password", { + chatId, + password: "", + }) + .then(() => { + toast.success("Senha removida!"); + }) + .catch((error) => { + toast.error(`Erro ao remover senha: ${error.response.data.message}`); + }) + .finally(() => { + setIsLoading(false); + setIsOpen(false); + handleHideLock(); + }); + } + }; + + return ( + <> + + + + + +
+ + +
+
+ + + + Editando senha do chat + +
+
+ + {errors.password && ( + + Campo obrigatório + + )} +
+ +
+ +
+
+
+
+
+
+ + ); +} diff --git a/frontend/src/components/Chat/ListChannels.tsx b/frontend/src/components/Chat/ListChannels.tsx index f1df3391..26a08d24 100644 --- a/frontend/src/components/Chat/ListChannels.tsx +++ b/frontend/src/components/Chat/ListChannels.tsx @@ -1,14 +1,19 @@ import { Plus } from "@phosphor-icons/react"; import ChannelCard from "./ChannelCard"; import { Chat, ChatContext } from "@/contexts/ChatContext"; -import { useContext } from "react"; +import { use, useContext, useEffect } from "react"; type ListChannelsProps = { handleShowCreateChannel: () => void; }; export function ListChannels({ handleShowCreateChannel }: ListChannelsProps) { - const { isLoading, chatList } = useContext(ChatContext); + const { isLoading, chatList, handleUpdateListChats } = + useContext(ChatContext); + + useEffect(() => { + handleUpdateListChats(); + }, []); // filter private chats const publicChats = chatList?.filter( diff --git a/frontend/src/components/Chat/OpenChannel.tsx b/frontend/src/components/Chat/OpenChannel.tsx index 0ad183aa..d563f04e 100644 --- a/frontend/src/components/Chat/OpenChannel.tsx +++ b/frontend/src/components/Chat/OpenChannel.tsx @@ -10,6 +10,7 @@ import ChatUsersChannelPopOver, { ChatMember } from "./ChatUsersChannelPopOver"; import chatService from "@/services/chatClient"; import { useForm } from "react-hook-form"; import Link from "next/link"; +import ChangeChatPassword from "./ChangeChatPassword"; interface Message { id: number; content: string; @@ -36,8 +37,10 @@ export function OpenChannel() { const [numberOfUsersInChat, setNumberOfUsersInChat] = useState(0); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true); - const myUserList = users.filter((usr) => usr.userLogin === user.login); - const myUser = myUserList[0] || null; + const myUser = users.find((chatUser) => chatUser.userLogin === user.login); + const [showLock, setShowLock] = useState(() => + selectedChat.chatType === "PROTECTED" ? true : false + ); chatService.socket?.on("listMessages", (messages: Message[]) => { setMessages(() => messages); @@ -54,9 +57,16 @@ export function OpenChannel() { ); setNumberOfUsersInChat(currentMembers.length); setUsers(currentMembers); - console.log("listMembers", currentMembers); setIsLoading(false); }); + + // on chat open go to the bottom of the messages + setTimeout(() => { + const messagesContainer = document.getElementById("messages-container"); + if (messagesContainer) { + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } + }, 100); }, []); chatService.socket?.on("verifyPassword", (response: any) => { @@ -107,6 +117,10 @@ export function OpenChannel() { }); }; + const handleHideLock = () => { + setShowLock(false); + }; + if (selectedChat.chatType === "PROTECTED" && validationRequired) { return (
@@ -176,12 +190,28 @@ export function OpenChannel() { .filter((name) => name !== user.displayName)}` : selectedChat.name} - handleCloseChat(selectedChat.id)} - /> +
+ {showLock && myUser?.role === "OWNER" && ( + + )} + handleCloseChat(selectedChat.id)} + /> +
+
>; validationRequired: boolean; user: User; + handleUpdateListChats: () => void; }; type ChatProviderProps = { @@ -58,13 +59,15 @@ export const ChatProvider = ({ children }: ChatProviderProps) => { const handleOpenChannel = (chat: Chat) => { setSelectedChat(chat); // check if chat has an id - try { - if (chat.id) { - chatService.socket?.emit("listMessages", { chatId: chat.id }); - chatService.socket?.emit("listMembers", { chatId: chat.id }); - setShowElement("showChannelOpen"); - } - } catch (error) {}; // required to avoid raising an error when chat.id is undefined + if (chat?.id) { + chatService.socket?.emit("listMessages", { chatId: chat.id }); + chatService.socket?.emit("listMembers", { chatId: chat.id }); + } + setShowElement("showChannelOpen"); + }; + + const handleUpdateListChats = () => { + chatService.socket?.emit("listChats"); }; useEffect(() => { @@ -72,7 +75,6 @@ export const ChatProvider = ({ children }: ChatProviderProps) => { chatService.connect(); chatService.socket?.on("userLogin", (user: User) => { - console.log(`Current user login: ${user.login}`, user); setUser(() => user); queryClient.invalidateQueries(["user_status", user.id]); queryClient.invalidateQueries(["friends"]); @@ -151,6 +153,7 @@ export const ChatProvider = ({ children }: ChatProviderProps) => { setValidationRequired, validationRequired, user, + handleUpdateListChats, }} > {children}