Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/users status #97

Merged
merged 31 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6697c77
feat: add pino logger and update prisma to fix bug
iaurg Nov 8, 2023
7e7135a
feat: add global logger
iaurg Nov 8, 2023
9b2f216
feat: instantiate logger into chat
iaurg Nov 8, 2023
c88db9e
refactor: remove dev container
iaurg Nov 10, 2023
ad55259
feat: move post create command
iaurg Nov 10, 2023
9b4c267
refactor: simplify docker files
iaurg Nov 10, 2023
61671a6
feat: create a temporary db for dev
iaurg Nov 10, 2023
5dc98a5
refactor: move declarations
iaurg Nov 11, 2023
b452df9
refactor: logger configs
iaurg Nov 11, 2023
1e6c267
feat: add status into user connection
iaurg Nov 11, 2023
87f0df2
fix: default user status creation to offline
iaurg Nov 11, 2023
60055b2
feat: add status requests
iaurg Nov 11, 2023
82a947f
feat: return status and display name on leaderboard
iaurg Nov 11, 2023
08b02aa
feat: update status component to use status from server
iaurg Nov 11, 2023
ad9b21a
feat: update user type and revalidate query
iaurg Nov 11, 2023
48b739b
Feat/83-image-upload (#86)
iaurg Nov 11, 2023
b7b6ea8
Feat/user friends (#92)
iaurg Nov 11, 2023
6f930d1
feat: add global logger
iaurg Nov 8, 2023
a636b83
feat: create a temporary db for dev
iaurg Nov 10, 2023
fda7de0
feat: add statuses into list friend
iaurg Nov 11, 2023
8827db3
Feat/83-image-upload (#86)
iaurg Nov 11, 2023
f20390d
feat: add pino logger and update prisma to fix bug
iaurg Nov 8, 2023
4301db7
Feat/83-image-upload (#86)
iaurg Nov 11, 2023
4932e86
feat: add global logger
iaurg Nov 8, 2023
e89743f
Merge branch 'main' into feat/users-status
iaurg Nov 11, 2023
4430ea6
fix: rollback argon2
iaurg Nov 11, 2023
74fcc84
feat: add data into ignore
iaurg Nov 11, 2023
e913fad
Merge branch 'main' into feat/users-status
iaurg Nov 16, 2023
f9c84e6
fix: build error
iaurg Nov 18, 2023
e57dd3c
fix: duplicated value after merge
iaurg Nov 18, 2023
485b313
fix: duplicated import
iaurg Nov 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,4 @@ postgres/
=======
# Local Uploads
/backend/uploads/*
data
data
2,499 changes: 1,325 additions & 1,174 deletions backend/package-lock.json

Large diffs are not rendered by default.

142 changes: 79 additions & 63 deletions backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ interface ConnectedUsers {
export class ChatGateway
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
@WebSocketServer() server: Server;

constructor(
private chatService: ChatService,
private jwtService: JwtService,
Expand All @@ -40,8 +42,81 @@ export class ChatGateway
private connectedUsers: ConnectedUsers = {};
private readonly logger = new Logger(ChatGateway.name);

@WebSocketServer()
server: Server;
afterInit() {
this.logger.debug('Initialized chat gateway');
this.server.use((socket, next) => {
this.validateConnection(socket)
.then((user) => {
socket.handshake.auth['user'] = user;
socket.emit('userLogin', user);
next();
})
.catch((err) => {
this.logger.error(
`Failed to authenticate user: ${socket.handshake.auth?.user?.login}`,
err,
);
return next(new Error(err));
});
});
}

async handleDisconnect(client: Socket) {
const { id } = client.handshake.auth?.user;

for (const userId in this.connectedUsers) {
if (this.connectedUsers[userId] === client) {
delete this.connectedUsers[userId];
this.logger.log(`User ${userId} disconnected`);
break;
}
}

// change user status to offline
await this.usersService.updateUserStatus(id, 'OFFLINE');
}

// TODO:
async handleConnection(@ConnectedSocket() client: Socket) {
const { login, id } = client.handshake.auth?.user;

// TODO: remove this hardcoded user id
if (!login) {
client.emit('connected', { error: 'User not found' });
client.disconnect();
return;
}

// change user status to online
await this.usersService.updateUserStatus(id, 'ONLINE');

this.connectedUsers[login] = client;
client.emit('connected', { message: `You are connected as ${login}` });

// const allChats = await this.chatService.listChats();
const chats = await this.chatService.listChatsByUserLogin(login);

// TODO: Remove this after implementing chat rooms
for (const chat of chats) {
client.join(`chat:${chat.id.toString()}`);
}

this.logger.log(`User ${login} connected`);
}

private validateConnection(client: Socket) {
const token = client.handshake.auth.token;

try {
const payload = this.jwtService.verify<TokenPayload>(token, {
secret: process.env.JWT_SECRET,
});
return this.usersService.findOne(payload.sub);
} catch {
this.logger.error('Token invalid or expired');
throw new WsException('Token invalid or expired');
}
}

async addConnectedUsersToChat(chatId: number) {
const users = await this.chatService.getUsersByChatId(chatId);
Expand All @@ -62,10 +137,12 @@ export class ChatGateway
const login = client.handshake.auth?.user?.login;
client.emit('userLogin', client.handshake.auth?.user);
const member = await this.chatService.getMemberFromChat(chatId, login);

if (!member) {
client.emit('error', { error: 'You are not a member of this chat' });
return;
}

if (member.status !== 'ACTIVE') {
client.emit('error', { error: 'You are not allowed to send messages' });
return;
Expand Down Expand Up @@ -522,65 +599,4 @@ export class ChatGateway
);
return numberOfUsers;
}

handleDisconnect(client: Socket) {
for (const userId in this.connectedUsers) {
if (this.connectedUsers[userId] === client) {
delete this.connectedUsers[userId];
console.log(`User ${userId} disconnected`);
break;
}
}
}
// TODO:
async handleConnection(@ConnectedSocket() client: Socket) {
const login = client.handshake.auth?.user?.login;
// TODO: remove this hardcoded user id
if (!login) {
client.emit('connected', { error: 'User not found' });
client.disconnect();
return;
}
this.connectedUsers[login] = client;
client.emit('connected', { message: `You are connected as ${login}` });
const allChats = await this.chatService.listChats();
const chats = await this.chatService.listChatsByUserLogin(login);
console.log(
`User ${login} joined ${chats.length}/${allChats.length} chats`,
);
// TODO: Remove this after implementing chat rooms
for (const chat of chats) {
client.join(`chat:${chat.id.toString()}`);
}
}

afterInit(_: Server) {
this.server.use((socket, next) => {
this.validateConnection(socket)
.then((user) => {
socket.handshake.auth['user'] = user;
console.log(`User ${socket.handshake.auth['user'].login} connected`);
socket.emit('userLogin', user);
next();
})
.catch((err) => {
this.logger.error(err);
return next(new Error(err));
});
});
}

private validateConnection(client: Socket) {
const token = client.handshake.auth.token;

try {
const payload = this.jwtService.verify<TokenPayload>(token, {
secret: process.env.JWT_SECRET,
});
return this.usersService.findOne(payload.sub);
} catch {
this.logger.error('Token invalid or expired');
throw new WsException('Token invalid or expired');
}
}
}
2 changes: 1 addition & 1 deletion backend/src/database/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ model User {
displayName String
email String
avatar String?
status UserStatus @default(ONLINE)
status UserStatus @default(OFFLINE)
victory Int @default(0)
refreshToken String?
mfaEnabled Boolean @default(false)
Expand Down
2 changes: 2 additions & 0 deletions backend/src/leaderboard/leaderboard.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ export class LeaderboardService {
},
select: {
displayName: true,
login: true,
victory: true,
avatar: true,
id: true,
status: true,
},
});
}
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ async function bootstrap() {

// Set up cookie parser
app.use(cookieParser());

app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);

app.enableCors({
origin: process.env.FRONTEND_URL,
credentials: true,
Expand Down
5 changes: 5 additions & 0 deletions backend/src/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export class UsersController {
return this.service.findOne(login);
}

@Get('status/:id')
findStatus(@Param('id') id: string) {
return this.service.getUserStatus(id);
}

@Get()
findAll() {
return this.service.findAll();
Expand Down
30 changes: 28 additions & 2 deletions backend/src/users/users.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateUserDto } from './dto/createUser.dto';
import { UpdateUserDto } from './dto/updateUser.dto';
import { User } from '@prisma/client';
import { User, UserStatus } from '@prisma/client';

@Injectable()
export class UsersService {
Expand Down Expand Up @@ -63,4 +63,30 @@ export class UsersService {
},
});
}

async getUserStatus(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
status: true,
},
});

if (!user) {
throw new NotFoundException('User not found');
}

return {
status: user.status,
};
}

async updateUserStatus(userId: string, status: UserStatus) {
return await this.prisma.user.update({
where: { id: userId },
data: {
status: status,
},
});
}
}
27 changes: 20 additions & 7 deletions frontend/src/components/Chat/FriendCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { api } from "@/services/apiClient";
import { queryClient } from "@/services/queryClient";
import { UserStatus } from "@/types/user";
import { EnvelopeSimple, Sword, UserMinus } from "@phosphor-icons/react";
import { useMutation } from "@tanstack/react-query";
import Link from "next/link";
Expand All @@ -8,9 +9,14 @@ import toast from "react-hot-toast";
type FriendCardProps = {
displayName: string;
id: string;
status: UserStatus;
};

export default function FriendCard({ displayName, id }: FriendCardProps) {
export default function FriendCard({
displayName,
id,
status,
}: FriendCardProps) {
const deleteFriendMutation = useMutation({
mutationFn: (friendData: any) => {
return api.delete("/friends", {
Expand All @@ -32,12 +38,19 @@ export default function FriendCard({ displayName, id }: FriendCardProps) {

return (
<div className="bg-black42-200 flex justify-between rounded-lg items-center p-3 my-1">
<Link
href={`/game/history/${id}`}
className="flex space-x-2 items-center"
>
{displayName}
</Link>
<div className="flex justify-between items-center cursor-pointer gap-2">
<Link
href={`/game/history/${id}`}
className="flex space-x-2 items-center"
>
<div className="flex space-x-2 items-center">{displayName}</div>
</Link>
<div
className={`${
status === "ONLINE" ? "bg-green-500" : "bg-gray-500"
} w-2 h-2 rounded-full`}
></div>
</div>
<div className="flex space-x-5 items-center">
<EnvelopeSimple className="text-purple42-200" size={18} />
<Sword className="text-purple42-200" size={18} />
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/Chat/ListFriends.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ export function ListFriends() {
</div>
) : (
data?.friends.map((user: User) => (
<FriendCard key={user.id} displayName={user.displayName} id={user.id} />
<FriendCard
key={user.id}
displayName={user.displayName}
id={user.id}
status={user.status}
/>

))
)}
</div>
Expand Down
7 changes: 2 additions & 5 deletions frontend/src/components/LeaderBoard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,9 @@ export function LeaderBoard() {
{data?.map((user: User) => (
<LeaderBoardCard
key={user.id}
id={user.id}
name={user.displayName}
avatar={user.avatar || ""}
score={user.victory}
//TODO: create functional logic
isFriend={false}
isFriend={user.login.includes("m")}
user={user}
/>
))}
</>
Expand Down
17 changes: 7 additions & 10 deletions frontend/src/components/LeaderBoardCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,26 @@ import { Trophy, UserPlus } from "@phosphor-icons/react";
import { StatusTag } from "../StatusTag";
import ProfilePopOver from "../ProfilePopOver";
import UserAvatar from "../UserAvatar";
import { User } from "@/types/user";

type LeaderBoardCardProps = {
id: string;
name: string;
avatar: string;
score: number;
isFriend: boolean;
user: User;
};

export function LeaderBoardCard({
id,
name,
avatar,
score,
isFriend,
user,
}: LeaderBoardCardProps) {
return (
<div className="flex flex-row justify-between items-center py-3 bg-black42-200 p-4 rounded-lg w-full">
<ProfilePopOver name={name} score={score} id={id}>
<ProfilePopOver name={user.login} score={score} id={user.id}>
<div className="flex flex-row justify-between items-center">
<UserAvatar imageUrl={avatar} login={name} />
<div className="text-white text-lg mx-3">{name}</div>
<StatusTag status="offline" />
<UserAvatar imageUrl={user.avatar || ""} login={user.login} />
<div className="text-white text-lg mx-3">{user.displayName}</div>
<StatusTag user={user} />
</div>
</ProfilePopOver>
<div className="flex flex-row justify-between items-center space-x-4">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function Sidebar() {
</div>
<ul className="flex flex-col space-y-5 items-center justify-center pb-4">
<li>
<UserAvatar imageUrl={user.avatar} login={user.displayName} />
<UserAvatar imageUrl={user.avatar || ""} login={user.displayName} />
</li>
<li>
<Link
Expand Down
Loading
Loading