diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 0e09a32b..31708d23 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Delete, Get, HttpCode, Post, @@ -16,14 +17,14 @@ import { ApiTags, } from '@nestjs/swagger'; import type { User } from '@prisma/client'; +import { Response } from 'express'; import { CurrentUser } from 'src/common/decorators/current-user.decorator'; import { AuthService } from './auth.service'; import { LoginDto } from './dto/login.dto'; import { TwoFactorAuthenticationDto } from './dto/twoFactorAuthentication.dto'; import { TwoFactorAuthenticationEnableDto } from './dto/twoFactorAuthenticationEnable.dto'; import { AuthEntity } from './entity/auth.entity'; -import { JwtGuardWithout2FA } from './jwt-auth.guard'; -import { Response } from 'express'; +import { JwtAuthGuard, JwtGuardWithout2FA } from './jwt-auth.guard'; const constants = { loginUrl: ((): string => { @@ -121,4 +122,13 @@ export class AuthController { ) { return this.authService.twoFactorAuthenticate(dto, user.id); } + + @Delete('2fa/disable') + @HttpCode(200) + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse() + async disable2FA(@CurrentUser() user: User) { + return this.authService.disableTwoFactorAuthentication(user.id); + } } diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index bb642124..dfc2299d 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -224,6 +224,21 @@ export class AuthService { }); } + disableTwoFactorAuthentication(userId: number) { + return this.prisma.$transaction(async (prisma) => { + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user.twoFactorEnabled) { + throw new ConflictException('2FA secret is not enabled'); + } + await prisma.user.update({ + where: { id: user.id }, + data: { + twoFactorEnabled: false, + }, + }); + }); + } + async twoFactorAuthenticate(dto: TwoFactorAuthenticationDto, userId: number) { const user = await this.prisma.user.findUnique({ where: { id: userId } }); if (!user.twoFactorEnabled) { diff --git a/backend/src/chat/chat.gateway.ts b/backend/src/chat/chat.gateway.ts index ab8479e7..b7684dd8 100644 --- a/backend/src/chat/chat.gateway.ts +++ b/backend/src/chat/chat.gateway.ts @@ -10,15 +10,15 @@ import { import { Server, Socket } from 'socket.io'; import { RoomDeletedEvent } from 'src/common/events/room-deleted.event'; import { RoomEnteredEvent } from 'src/common/events/room-entered.event'; +import { RoomLeftEvent } from 'src/common/events/room-left.event'; import { RoomMuteEvent } from 'src/common/events/room-mute.event'; import { RoomUnmuteEvent } from 'src/common/events/room-unmute.event'; -import { RoomLeftEvent } from 'src/common/events/room-left.event'; import { RoomUpdateRoleEvent } from 'src/common/events/room-update-role.event'; -import { ChatService, UserStatus } from './chat.service'; import { MuteService } from 'src/room/mute/mute.service'; +import { v4 } from 'uuid'; +import { ChatService, UserStatus } from './chat.service'; import { CreateMessageDto } from './dto/create-message.dto'; import { MessageEntity } from './entities/message.entity'; -import { v4 } from 'uuid'; @WebSocketGateway({ cors: { diff --git a/backend/src/chat/chat.module.ts b/backend/src/chat/chat.module.ts index ee4a2bca..4d6657e3 100644 --- a/backend/src/chat/chat.module.ts +++ b/backend/src/chat/chat.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { AuthModule } from 'src/auth/auth.module'; -import { MuteService } from 'src/room/mute/mute.service'; import { PrismaModule } from 'src/prisma/prisma.module'; +import { MuteService } from 'src/room/mute/mute.service'; import { UserService } from '../user/user.service'; import { ChatController } from './chat.controller'; import { ChatGateway } from './chat.gateway'; diff --git a/backend/src/room/guards/enter-room.guard.ts b/backend/src/room/guards/enter-room.guard.ts index a691b05c..f1815cca 100644 --- a/backend/src/room/guards/enter-room.guard.ts +++ b/backend/src/room/guards/enter-room.guard.ts @@ -5,8 +5,8 @@ import { ForbiddenException, Injectable, } from '@nestjs/common'; -import { RoomService } from '../room.service'; import { compare } from 'bcrypt'; +import { RoomService } from '../room.service'; @Injectable() export class EnterRoomGuard implements CanActivate { diff --git a/backend/src/room/mute/mute.controller.ts b/backend/src/room/mute/mute.controller.ts index 466b0290..334ae90b 100644 --- a/backend/src/room/mute/mute.controller.ts +++ b/backend/src/room/mute/mute.controller.ts @@ -1,17 +1,17 @@ import { - Controller, - Get, - Put, Body, + Controller, Delete, + Get, Param, ParseIntPipe, + Put, UseGuards, } from '@nestjs/common'; -import { CreateMuteDto } from './dto/create-mute.dto'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; import { AdminGuard } from '../guards/admin.guard'; +import { CreateMuteDto } from './dto/create-mute.dto'; import { MuteService } from './mute.service'; @ApiTags('mute') diff --git a/backend/src/room/mute/mute.module.ts b/backend/src/room/mute/mute.module.ts index 83ee5719..59420272 100644 --- a/backend/src/room/mute/mute.module.ts +++ b/backend/src/room/mute/mute.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { PrismaModule } from 'src/prisma/prisma.module'; -import { MuteService } from './mute.service'; import { MuteController } from './mute.controller'; +import { MuteService } from './mute.service'; @Module({ controllers: [MuteController], diff --git a/backend/src/room/mute/mute.service.ts b/backend/src/room/mute/mute.service.ts index 88910e87..247ba3bd 100644 --- a/backend/src/room/mute/mute.service.ts +++ b/backend/src/room/mute/mute.service.ts @@ -4,11 +4,11 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { CreateMuteDto } from './dto/create-mute.dto'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { RoomMuteEvent } from 'src/common/events/room-mute.event'; import { RoomUnmuteEvent } from 'src/common/events/room-unmute.event'; import { PrismaService } from 'src/prisma/prisma.service'; +import { CreateMuteDto } from './dto/create-mute.dto'; @Injectable() export class MuteService { diff --git a/backend/src/room/room.service.ts b/backend/src/room/room.service.ts index 8a764757..db7b610f 100644 --- a/backend/src/room/room.service.ts +++ b/backend/src/room/room.service.ts @@ -5,6 +5,7 @@ import { } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { Role, User } from '@prisma/client'; +import { hash } from 'bcrypt'; import { RoomCreatedEvent } from 'src/common/events/room-created.event'; import { RoomDeletedEvent } from 'src/common/events/room-deleted.event'; import { RoomEnteredEvent } from 'src/common/events/room-entered.event'; @@ -16,7 +17,6 @@ import { UpdateUserOnRoomDto } from './dto/update-UserOnRoom.dto'; import { UpdateRoomDto } from './dto/update-room.dto'; import { UserOnRoomEntity } from './entities/UserOnRoom.entity'; import { RoomEntity } from './entities/room.entity'; -import { hash } from 'bcrypt'; @Injectable() export class RoomService { diff --git a/backend/test/auth.e2e-spec.ts b/backend/test/auth.e2e-spec.ts index bd694cc7..9f4566e2 100644 --- a/backend/test/auth.e2e-spec.ts +++ b/backend/test/auth.e2e-spec.ts @@ -77,5 +77,16 @@ describe('AuthController (e2e)', () => { it('[GET /user/me] should return 200 if 2FA is enabled and code is provided', async () => { await app.getMe(user.accessToken).expect(200); }); + + it('[DELETE /auth/2fa/disable] should disable 2FA', async () => { + await app.disableTwoFactorAuthentication(user.accessToken).expect(200); + }); + + it('[POST /auth/2fa/enable] should re-enable 2FA', async () => { + const code = authenticator.generate(secret); + await app + .enableTwoFactorAuthentication(code, user.accessToken) + .expect(200); + }); }); }); diff --git a/backend/test/chat-gateway.e2e-spec.ts b/backend/test/chat-gateway.e2e-spec.ts index ed02da8d..74331b9d 100644 --- a/backend/test/chat-gateway.e2e-spec.ts +++ b/backend/test/chat-gateway.e2e-spec.ts @@ -1,12 +1,12 @@ -import { Role } from '@prisma/client'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { Role } from '@prisma/client'; import { Socket, io } from 'socket.io-client'; import { AppModule } from 'src/app.module'; import { MessageEntity } from 'src/chat/entities/message.entity'; +import { CreateRoomDto } from 'src/room/dto/create-room.dto'; import { constants } from './constants'; import { TestApp, UserEntityWithAccessToken } from './utils/app'; -import { CreateRoomDto } from 'src/room/dto/create-room.dto'; async function createNestApp(): Promise { const moduleFixture: TestingModule = await Test.createTestingModule({ diff --git a/backend/test/online-status.e2e-spec.ts b/backend/test/online-status.e2e-spec.ts index f423e9c6..237551af 100644 --- a/backend/test/online-status.e2e-spec.ts +++ b/backend/test/online-status.e2e-spec.ts @@ -2,9 +2,9 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Socket, io } from 'socket.io-client'; import { AppModule } from 'src/app.module'; +import { UserStatus } from 'src/chat/chat.service'; import { TestApp, UserEntityWithAccessToken } from './utils/app'; import { expectOnlineStatusResponse } from './utils/matcher'; -import { UserStatus } from 'src/chat/chat.service'; type UserAndSocket = { user: UserEntityWithAccessToken; diff --git a/backend/test/utils/app.ts b/backend/test/utils/app.ts index b4a7c0a9..62f23f75 100644 --- a/backend/test/utils/app.ts +++ b/backend/test/utils/app.ts @@ -29,6 +29,11 @@ export class TestApp { .set('Authorization', `Bearer ${accessToken}`) .send({ code }); + disableTwoFactorAuthentication = (accessToken: string) => + request(this.app.getHttpServer()) + .delete('/auth/2fa/disable') + .set('Authorization', `Bearer ${accessToken}`); + twoFactorAuthenticate = (code: string, accessToken: string) => request(this.app.getHttpServer()) .post('/auth/2fa/authenticate') diff --git a/frontend/app/explore-rooms/explore-rooms.tsx b/frontend/app/explore-rooms/explore-rooms.tsx index 37f35396..4d55b8b0 100644 --- a/frontend/app/explore-rooms/explore-rooms.tsx +++ b/frontend/app/explore-rooms/explore-rooms.tsx @@ -1,10 +1,10 @@ "use client"; -import RoomCard from "./room-card"; -import { useCallback, useEffect } from "react"; -import { useRouter } from "next/navigation"; import { DeleteRoomEvent, RoomEntity } from "@/app/lib/dtos"; import { chatSocket as socket } from "@/socket"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect } from "react"; +import RoomCard from "./room-card"; export function ExploreRooms({ rooms }: { rooms: RoomEntity[] }) { const router = useRouter(); diff --git a/frontend/app/lib/actions.ts b/frontend/app/lib/actions.ts index 8b38a226..b97598d9 100644 --- a/frontend/app/lib/actions.ts +++ b/frontend/app/lib/actions.ts @@ -106,21 +106,30 @@ export async function updateUser( prevState: string | undefined, formData: FormData, ) { - const { user_id, ...updateData } = Object.fromEntries(formData.entries()); - const res = await fetch(`${process.env.API_URL}/user/${user_id}`, { + const userId = await getCurrentUserId(); + const newNickname = formData.get("name"); + const newEmail = formData.get("email"); + const res = await fetch(`${process.env.API_URL}/user/${userId}`, { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: "Bearer " + getAccessToken(), }, - body: JSON.stringify(updateData), + body: JSON.stringify({ name: newNickname, email: newEmail }), }); const data = await res.json(); + if (res.status === 401) { + redirect("/login"); + } else if (res.status === 409) { + console.error("updateUser error: ", data); + return "The name or email is already in use"; + } if (!res.ok) { - return "Error"; + console.error("updateUser error: ", data); + return data.message; } else { revalidatePath("/user"); - revalidatePath(`/user/${user_id}`); + revalidatePath(`/user/${userId}`); return "Success"; } } diff --git a/frontend/app/lib/client-socket-provider.tsx b/frontend/app/lib/client-socket-provider.tsx index 4166a55e..0056bdb3 100644 --- a/frontend/app/lib/client-socket-provider.tsx +++ b/frontend/app/lib/client-socket-provider.tsx @@ -1,9 +1,10 @@ "use client"; import { ToastAction } from "@/components/ui/toast"; import { useToast } from "@/components/ui/use-toast"; -import { chatSocket } from "@/socket"; +import { chatSocket, chatSocket as socket } from "@/socket"; import Link from "next/link"; -import { useCallback, useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { useEffect } from "react"; import { useAuthContext } from "./client-auth"; import { DenyEvent, @@ -12,9 +13,6 @@ import { MessageEvent, PublicUserEntity, } from "./dtos"; -import { chatSocket as socket } from "@/socket"; -import { useRouter } from "next/navigation"; -import { usePathname } from "next/navigation"; export default function SocketProvider() { const { toast } = useToast(); diff --git a/frontend/app/lib/hooks/useInviteToGame.ts b/frontend/app/lib/hooks/useInviteToGame.ts index fa752c36..e507be96 100644 --- a/frontend/app/lib/hooks/useInviteToGame.ts +++ b/frontend/app/lib/hooks/useInviteToGame.ts @@ -1,7 +1,7 @@ "use client"; -import { useCallback, useState } from "react"; import { chatSocket as socket } from "@/socket"; +import { useCallback, useState } from "react"; export const useInviteToGame = (userId: number) => { const [isInvitingToGame, setIsInvitingToGame] = useState(false); diff --git a/frontend/app/lib/hooks/useKick.ts b/frontend/app/lib/hooks/useKick.ts index edd9e519..50f1375a 100644 --- a/frontend/app/lib/hooks/useKick.ts +++ b/frontend/app/lib/hooks/useKick.ts @@ -1,12 +1,11 @@ "use client"; import { kickUserOnRoom } from "@/app/lib/actions"; -import type { LeaveRoomEvent, UserOnRoomEntity } from "@/app/lib/dtos"; +import type { LeaveRoomEvent } from "@/app/lib/dtos"; import { toast } from "@/components/ui/use-toast"; -import { useCallback, useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; import { chatSocket as socket } from "@/socket"; -import { usePathname } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; const showKickErrorToast = () => { toast({ diff --git a/frontend/app/lib/hooks/useMute.ts b/frontend/app/lib/hooks/useMute.ts index 03f870fd..9c906c28 100644 --- a/frontend/app/lib/hooks/useMute.ts +++ b/frontend/app/lib/hooks/useMute.ts @@ -3,8 +3,8 @@ import { muteUser, unmuteUser } from "@/app/lib/actions"; import type { PublicUserEntity } from "@/app/lib/dtos"; import { toast } from "@/components/ui/use-toast"; -import { useCallback, useEffect, useState } from "react"; import { chatSocket as socket } from "@/socket"; +import { useCallback, useEffect, useState } from "react"; interface MuteEvent { userId: number; diff --git a/frontend/app/lib/hooks/useOnlineStatus.ts b/frontend/app/lib/hooks/useOnlineStatus.ts index 2b6e9840..006fe1fc 100644 --- a/frontend/app/lib/hooks/useOnlineStatus.ts +++ b/frontend/app/lib/hooks/useOnlineStatus.ts @@ -1,7 +1,7 @@ "use client"; -import { createContext, useEffect, useState } from "react"; import { chatSocket } from "@/socket"; +import { createContext, useEffect, useState } from "react"; export const OnlineContext = createContext<{ [key: number]: number }>({}); diff --git a/frontend/app/lib/hooks/useUpdateRole.ts b/frontend/app/lib/hooks/useUpdateRole.ts index e6b2e634..8ef5690a 100644 --- a/frontend/app/lib/hooks/useUpdateRole.ts +++ b/frontend/app/lib/hooks/useUpdateRole.ts @@ -3,8 +3,8 @@ import { updateRoomUser } from "@/app/lib/actions"; import type { UserOnRoomEntity } from "@/app/lib/dtos"; import { toast } from "@/components/ui/use-toast"; -import { useCallback, useEffect, useState } from "react"; import { chatSocket as socket } from "@/socket"; +import { useCallback, useEffect, useState } from "react"; type Role = "ADMINISTRATOR" | "MEMBER"; diff --git a/frontend/app/onlineProviders.tsx b/frontend/app/onlineProviders.tsx index ae6e2cc4..16310490 100644 --- a/frontend/app/onlineProviders.tsx +++ b/frontend/app/onlineProviders.tsx @@ -1,7 +1,7 @@ "use client"; -import { OnlineContext, useOnlineStatus } from "./lib/hooks/useOnlineStatus"; import SocketProvider from "./lib/client-socket-provider"; +import { OnlineContext, useOnlineStatus } from "./lib/hooks/useOnlineStatus"; export function OnlineProviders({ children }: { children: React.ReactNode }) { const onlineStatus = useOnlineStatus(); diff --git a/frontend/app/pong/JoinRoomForm.tsx b/frontend/app/pong/JoinRoomForm.tsx index 16a5ba38..9d13c438 100644 --- a/frontend/app/pong/JoinRoomForm.tsx +++ b/frontend/app/pong/JoinRoomForm.tsx @@ -10,12 +10,12 @@ import { } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; -import MatchButton from "./MatchButton"; import { v4 } from "uuid"; -import { Separator } from "@/components/ui/separator"; +import MatchButton from "./MatchButton"; export default function JoinRoomForm({}) { return ( diff --git a/frontend/app/room/[id]/mute-menu.tsx b/frontend/app/room/[id]/mute-menu.tsx index fecc69b4..6c2b4678 100644 --- a/frontend/app/room/[id]/mute-menu.tsx +++ b/frontend/app/room/[id]/mute-menu.tsx @@ -1,13 +1,11 @@ "use client"; -import { muteUser, unmuteUser } from "@/app/lib/actions"; import { ContextMenuItem, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, } from "@/components/ui/context-menu"; -import { useState } from "react"; export default function MuteMenu({ isMuted, diff --git a/frontend/app/room/[id]/sidebar-item.tsx b/frontend/app/room/[id]/sidebar-item.tsx index 5506d8c9..ba6fd317 100644 --- a/frontend/app/room/[id]/sidebar-item.tsx +++ b/frontend/app/room/[id]/sidebar-item.tsx @@ -7,6 +7,11 @@ import type { RoomEntity, UserOnRoomEntity, } from "@/app/lib/dtos"; +import { useBlock } from "@/app/lib/hooks/useBlock"; +import { useInviteToGame } from "@/app/lib/hooks/useInviteToGame"; +import { useKick } from "@/app/lib/hooks/useKick"; +import { useMute } from "@/app/lib/hooks/useMute"; +import { useUpdateRole } from "@/app/lib/hooks/useUpdateRole"; import { Avatar } from "@/app/ui/user/avatar"; import { ContextMenu, @@ -15,15 +20,10 @@ import { ContextMenuSeparator, ContextMenuTrigger, } from "@/components/ui/context-menu"; -import { useCallback, useEffect } from "react"; +import { chatSocket as socket } from "@/socket"; import { useRouter } from "next/navigation"; -import { useBlock } from "@/app/lib/hooks/useBlock"; -import { useInviteToGame } from "@/app/lib/hooks/useInviteToGame"; -import { useKick } from "@/app/lib/hooks/useKick"; -import { useMute } from "@/app/lib/hooks/useMute"; -import { useUpdateRole } from "@/app/lib/hooks/useUpdateRole"; +import { useCallback, useEffect } from "react"; import MuteMenu from "./mute-menu"; -import { chatSocket as socket } from "@/socket"; function truncateString(str: string | undefined, num: number): string { if (!str) { diff --git a/frontend/app/room/rooms-sidebar.tsx b/frontend/app/room/rooms-sidebar.tsx index be78908a..ffac0290 100644 --- a/frontend/app/room/rooms-sidebar.tsx +++ b/frontend/app/room/rooms-sidebar.tsx @@ -1,12 +1,12 @@ "use client"; +import { useAuthContext } from "@/app/lib/client-auth"; import type { EnterRoomEvent, RoomEntity } from "@/app/lib/dtos"; import { Stack } from "@/components/layout/stack"; +import { chatSocket as socket } from "@/socket"; import Link from "next/link"; -import { useCallback, useEffect } from "react"; import { usePathname, useRouter } from "next/navigation"; +import { useCallback, useEffect } from "react"; import CreateRoomDialog from "./create-room-dialog"; -import { useAuthContext } from "@/app/lib/client-auth"; -import { chatSocket as socket } from "@/socket"; function RoomButton({ room, diff --git a/frontend/app/ui/auth/login-form.tsx b/frontend/app/ui/auth/login-form.tsx index 9afaf4c2..78073afb 100644 --- a/frontend/app/ui/auth/login-form.tsx +++ b/frontend/app/ui/auth/login-form.tsx @@ -3,19 +3,13 @@ import { authenticate } from "@/app/lib/actions"; import { useAuthContext } from "@/app/lib/client-auth"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { useFormState, useFormStatus } from "react-dom"; +import { Separator } from "@/components/ui/separator"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; -import { Separator } from "@/components/ui/separator"; +import { useFormState, useFormStatus } from "react-dom"; export default function LoginForm() { return ( diff --git a/frontend/app/ui/room/ban-item.tsx b/frontend/app/ui/room/ban-item.tsx index 7dcd1e45..4358ce44 100644 --- a/frontend/app/ui/room/ban-item.tsx +++ b/frontend/app/ui/room/ban-item.tsx @@ -4,8 +4,8 @@ import { banUser } from "@/app/lib/actions"; import { PublicUserEntity } from "@/app/lib/dtos"; import { Avatar } from "@/app/ui/user/avatar"; import Loader from "@/components/ui/loader"; -import { Ban } from "lucide-react"; import { toast } from "@/components/ui/use-toast"; +import { Ban } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; diff --git a/frontend/app/ui/room/invite-item.tsx b/frontend/app/ui/room/invite-item.tsx index 46468f5a..7596d297 100644 --- a/frontend/app/ui/room/invite-item.tsx +++ b/frontend/app/ui/room/invite-item.tsx @@ -4,8 +4,8 @@ import { inviteUserToRoom } from "@/app/lib/actions"; import { PublicUserEntity } from "@/app/lib/dtos"; import { Avatar } from "@/app/ui/user/avatar"; import Loader from "@/components/ui/loader"; -import { LogIn } from "lucide-react"; import { toast } from "@/components/ui/use-toast"; +import { LogIn } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; diff --git a/frontend/app/ui/room/invite-modal.tsx b/frontend/app/ui/room/invite-modal.tsx index 76993a63..bb24a9b0 100644 --- a/frontend/app/ui/room/invite-modal.tsx +++ b/frontend/app/ui/room/invite-modal.tsx @@ -9,7 +9,6 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { LogIn } from "lucide-react"; import InviteItem from "./invite-item"; interface Props { diff --git a/frontend/app/ui/room/unban-item.tsx b/frontend/app/ui/room/unban-item.tsx index a2b2d7bc..f631d11c 100644 --- a/frontend/app/ui/room/unban-item.tsx +++ b/frontend/app/ui/room/unban-item.tsx @@ -4,8 +4,8 @@ import { unbanUser } from "@/app/lib/actions"; import { PublicUserEntity } from "@/app/lib/dtos"; import { Avatar } from "@/app/ui/user/avatar"; import Loader from "@/components/ui/loader"; -import { CheckCircle2 } from "lucide-react"; import { toast } from "@/components/ui/use-toast"; +import { CheckCircle2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; diff --git a/frontend/app/ui/settings/profile-form.tsx b/frontend/app/ui/settings/profile-form.tsx index d668da43..3865a074 100644 --- a/frontend/app/ui/settings/profile-form.tsx +++ b/frontend/app/ui/settings/profile-form.tsx @@ -1,23 +1,45 @@ "use client"; +import { updateUser } from "@/app/lib/actions"; import { useAuthContext } from "@/app/lib/client-auth"; import { Stack } from "@/components/layout/stack"; import { Button } from "@/components/ui/button"; +import { toast } from "@/components/ui/use-toast"; +import { useFormState, useFormStatus } from "react-dom"; import AvatarForm from "./avatar-form"; import { ProfileItem } from "./profile-item-form"; +function ErrorText({ text }: { text: string }) { + return ( +

+ {text} +

+ ); +} + export default function ProfileForm() { const { currentUser } = useAuthContext(); + const [code, action] = useFormState(updateUser, undefined); + const { pending } = useFormStatus(); + if (code === "Success") { + toast({ title: "Success", description: "Profile updated sccessfully." }); + } + // Menu: min 100px // Profile : the rest return ( <> -
+ -
-
diff --git a/frontend/app/ui/user/user-list.tsx b/frontend/app/ui/user/user-list.tsx index 7cec9a5b..9e16a38d 100644 --- a/frontend/app/ui/user/user-list.tsx +++ b/frontend/app/ui/user/user-list.tsx @@ -1,7 +1,9 @@ "use client"; import type { PublicUserEntity } from "@/app/lib/dtos"; +import { OnlineContext } from "@/app/lib/hooks/useOnlineStatus"; import { TooltipProvider } from "@/components/ui/tooltip"; +import { useContext } from "react"; import { Avatar, AvatarSize } from "./avatar"; export default function UserList({