Skip to content

Commit

Permalink
Fix/backend/strict null check (#279)
Browse files Browse the repository at this point in the history
* [backend] Enable strictNullChecks in tsconfig.json

* [backend] Fix handleDisconnect to handle null user

* [backend] Fix MessageEntity constructor by strict null check

* [backend] Fix WsPublicUserEntity constructor by using the correct type

* [backend] Fix match request handlers

- Naming consistency
- Use match request instead of invite

* [backend] Fix UpdateUserOnRoomDto to not use PartialType

- because the properties are required, it should not use PartialType

* [backend] Fix RoomEntity password type to be nullable

- Align with the prisma schema

* [backend] Fix UserEntity password type to be nullable

- To align with prisma schema
- But it is not desirable to have nullable password field

* [backend] Fix PublicUserEntity password type to be nullable

* [backend] Fix mute and ban services to use findUniqueOrThrow

* [backend] Fix strict null check in enter-room.guard.ts

* [backend] Fix strict null check for events.gateway.ts

* [backend] Fix strict null check in auth service

* [backend] Fix strict null check in seed.ts

* [backend] Fix strict null check in main.ts

* [backend] Fix strict null check in events.gateway.ts

* [backend] Fix WsPublicUserEntity constructor

* [backend] Remove CreateUserOnRoomDto (which is not used)
  • Loading branch information
usatie authored Feb 21, 2024
1 parent a5ca428 commit c5c6495
Show file tree
Hide file tree
Showing 19 changed files with 162 additions and 105 deletions.
21 changes: 9 additions & 12 deletions backend/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,19 +163,16 @@ async function seedMatchHistory() {
}

async function seedFriends(users) {
let promises = [];
for (let i = 0; i < users.length - 1; i++) {
promises.push(
prisma.user.update({
where: { id: users[users.length - 1].id },
data: {
friends: {
connect: { id: users[i].id },
},
const promises = users.map((user) => {
return prisma.user.update({
where: { id: users[users.length - 1].id },
data: {
friends: {
connect: { id: user.id },
},
}),
);
}
},
});
});
await Promise.all(promises);
}

Expand Down
59 changes: 41 additions & 18 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import { TwoFactorAuthenticationDto } from './dto/twoFactorAuthentication.dto';
import { TwoFactorAuthenticationEnableDto } from './dto/twoFactorAuthenticationEnable.dto';
import { AuthEntity } from './entity/auth.entity';

const constants = {
appName: process.env.TWO_FACTOR_AUTHENTICATION_APP_NAME || 'Pong API',
clientId: process.env.OAUTH_42_CLIENT_ID || 'You need to set this',
clientSecret: process.env.OAUTH_42_CLIENT_SECRET || 'You need to set this',
publicURL: process.env.NEST_PUBLIC_API_URL || 'http://localhost:3000/api',
};

@Injectable()
export class AuthService {
constructor(
Expand All @@ -24,7 +31,7 @@ export class AuthService {
) {}

async login(email: string, password: string): Promise<AuthEntity> {
const user = await this.prisma.user.findUnique({ where: { email } });
const user = await this.prisma.user.findUniqueOrThrow({ where: { email } });

if (!user) {
throw new NotFoundException(`No user found for email: ${email}`);
Expand All @@ -36,6 +43,11 @@ export class AuthService {
);
}

// This should not happen : password should be set for all users except oauth users
if (!user.password) {
throw new UnauthorizedException('Password is not set for this user');
}

const isPasswordValid = await bcrypt.compare(password, user.password);

if (!isPasswordValid) {
Expand Down Expand Up @@ -66,14 +78,16 @@ export class AuthService {

async generateTwoFactorAuthenticationSecret(userId: number) {
return this.prisma.$transaction(async (prisma) => {
const user = await prisma.user.findUnique({ where: { id: userId } });
const user = await prisma.user.findUniqueOrThrow({
where: { id: userId },
});
if (user.twoFactorEnabled) {
throw new ConflictException('2FA secret is already enabled');
}
const secret = authenticator.generateSecret();
const otpAuthUrl = authenticator.keyuri(
user.email,
process.env.TWO_FACTOR_AUTHENTICATION_APP_NAME,
constants.appName,
secret,
);
await prisma.user.update({
Expand All @@ -93,10 +107,10 @@ export class AuthService {
}) {
const form = new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.OAUTH_42_CLIENT_ID,
client_secret: process.env.OAUTH_42_CLIENT_SECRET,
client_id: constants.clientId,
client_secret: constants.clientSecret,
code: code,
redirect_uri: process.env.NEST_PUBLIC_API_URL + redirect_uri,
redirect_uri: constants.publicURL + redirect_uri,
});

return fetch('https://api.intra.42.fr/oauth/token', {
Expand Down Expand Up @@ -190,23 +204,24 @@ export class AuthService {
return toFileStream(stream, otpAuthUrl);
}

isTwoFactorAuthenticationCodeValid(code: string, user: User) {
return authenticator.verify({ token: code, secret: user.twoFactorSecret });
}

enableTwoFactorAuthentication(
dto: TwoFactorAuthenticationEnableDto,
userId: number,
) {
return this.prisma.$transaction(async (prisma) => {
let user = await prisma.user.findUnique({ where: { id: userId } });
let user = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
if (user.twoFactorEnabled) {
throw new ConflictException('2FA secret is already enabled');
}
const isCodeValid = this.isTwoFactorAuthenticationCodeValid(
dto.code,
user,
);
if (!user.twoFactorSecret) {
throw new ConflictException(
'2FA secret is not generated for this user',
);
}
const isCodeValid = authenticator.verify({
token: dto.code,
secret: user.twoFactorSecret,
});
if (!isCodeValid) {
throw new UnauthorizedException('Invalid 2FA code');
}
Expand All @@ -226,7 +241,7 @@ export class AuthService {

disableTwoFactorAuthentication(userId: number) {
return this.prisma.$transaction(async (prisma) => {
let user = await prisma.user.findUnique({ where: { id: userId } });
let user = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
if (!user.twoFactorEnabled) {
throw new ConflictException('2FA secret is not enabled');
}
Expand All @@ -247,11 +262,19 @@ export class AuthService {
}

async twoFactorAuthenticate(dto: TwoFactorAuthenticationDto, userId: number) {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
const user = await this.prisma.user.findUniqueOrThrow({
where: { id: userId },
});
if (!user.twoFactorEnabled) {
throw new ConflictException('2FA secret is not enabled');
}
const isCodeValid = this.isTwoFactorAuthenticationCodeValid(dto.code, user);
if (!user.twoFactorSecret) {
throw new ConflictException('2FA secret is not generated for this user');
}
const isCodeValid = authenticator.verify({
token: dto.code,
secret: user.twoFactorSecret,
});
if (!isCodeValid) {
throw new UnauthorizedException('Invalid 2FA code');
}
Expand Down
80 changes: 54 additions & 26 deletions backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ export class ChatGateway {
this.logger.error('invalid userId');
return;
}
const user = this.chatService.getUser(client);
if (!user) {
this.logger.error('invalid user');
return;
}

const MutedUsers = await this.muteService.findAll(data.roomId);
if (MutedUsers.some((user) => user.id === data.userId)) {
Expand All @@ -68,44 +73,67 @@ export class ChatGateway {
const room = this.server
.to(data.roomId.toString())
.except('block' + data.userId);
room.emit(
'message',
new MessageEntity(data, this.chatService.getUser(client)),
);
room.emit('message', new MessageEntity(data, user));
}

@SubscribeMessage('request-match')
async handleRequestMatch(
@MessageBody() data: { userId: number },
@ConnectedSocket() client: Socket,
) {
const inviteUser = this.chatService.getUser(client);
const invitedUserWsId = this.chatService.getWsFromUserId(data.userId)?.id;
if (!invitedUserWsId) {
// Check if the requesting user is valid
const requestingUser = this.chatService.getUser(client);
if (!requestingUser) {
this.logger.error('invalid requesting user');
return;
}
// Check if the requested user is connected
const requestedUserWsId = this.chatService.getWsFromUserId(data.userId)?.id;
if (!requestedUserWsId) {
this.logger.error('invalid requested user');
return;
} else {
const blockings = await this.chatService.getUsersBlockedBy(data.userId);
if (blockings.some((user) => user.id === inviteUser.id)) return;
const blocked = await this.chatService.getUsersBlockedBy(inviteUser.id);
if (blocked.some((user) => user.id === data.userId)) return;
this.server
.to(invitedUserWsId)
.emit('request-match', { userId: inviteUser.id });
this.chatService.addInvite(inviteUser.id, data.userId);
}
// Check if the requesting user is blocked by the requested user
const blockings = await this.chatService.getUsersBlockedBy(data.userId);
if (blockings.some((user) => user.id === requestingUser.id)) return;
// Check if the requested user is blocked by the requesting user
const blocked = await this.chatService.getUsersBlockedBy(requestingUser.id);
if (blocked.some((user) => user.id === data.userId)) return;
// Send the request
this.server
.to(requestedUserWsId)
.emit('request-match', { userId: requestingUser.id });
// Save the request
this.chatService.addMatchRequest(requestingUser.id, data.userId);
}

@SubscribeMessage('cancel-request-match')
handleCancelRequestMatch(@ConnectedSocket() client: Socket) {
const inviteUser = this.chatService.getUser(client);
const invitee = this.chatService.getInvite(inviteUser.id);
if (!invitee) {
// Check if the requesting user is valid
const requestingUser = this.chatService.getUser(client);
if (!requestingUser) {
this.logger.error('invalid requesting user');
return;
}
// Check if the request exists
const requestedUser = this.chatService.getMatchRequest(requestingUser.id);
if (!requestedUser) {
this.logger.error('invalid requested user');
this.server.to(client.id).emit('error-pong', 'No pending invite found.');
return;
}
const inviteeWsId = this.chatService.getWsFromUserId(invitee)?.id;
this.chatService.removeInvite(inviteUser.id);
this.server.to(inviteeWsId).emit('cancel-request-match', inviteUser);
// Cancel the request
this.chatService.removeMatchRequest(requestingUser.id);
// Check if the requested user is connected
const requestedUserWsId =
this.chatService.getWsFromUserId(requestedUser)?.id;
if (!requestedUserWsId) {
return;
}
// Send the cancel request
this.server
.to(requestedUserWsId)
.emit('cancel-request-match', requestingUser);
}

@SubscribeMessage('approve-pong')
Expand All @@ -118,7 +146,7 @@ export class ChatGateway {
return;
} else {
if (
this.chatService.getInvite(data.userId) !==
this.chatService.getMatchRequest(data.userId) !==
this.chatService.getUserId(client)
) {
this.server
Expand All @@ -129,7 +157,7 @@ export class ChatGateway {
const emitData = { roomId: v4() };
this.server.to(client.id).emit('match-pong', emitData);
this.server.to(approvedUserWsId).emit('match-pong', emitData);
this.chatService.removeInvite(data.userId);
this.chatService.removeMatchRequest(data.userId);
}
}

Expand All @@ -143,7 +171,7 @@ export class ChatGateway {
return;
} else {
if (
this.chatService.getInvite(data.userId) !==
this.chatService.getMatchRequest(data.userId) !==
this.chatService.getUserId(client)
) {
this.server
Expand All @@ -152,7 +180,7 @@ export class ChatGateway {
return;
}
this.server.to(deniedUserWsId).emit('deny-pong');
this.chatService.removeInvite(data.userId);
this.chatService.removeMatchRequest(data.userId);
}
}

Expand Down
26 changes: 13 additions & 13 deletions backend/src/chat/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { UnblockEvent } from 'src/common/events/unblock.event';
import { PrismaService } from 'src/prisma/prisma.service';
import { UserService } from 'src/user/user.service';
import { CreateMessageDto } from './dto/create-message.dto';
import { PublicUserEntity } from './entities/message.entity';
import { WsPublicUserEntity } from './entities/message.entity';

export enum UserStatus {
Offline = 0b0,
Expand All @@ -35,9 +35,9 @@ export class ChatService {

// Map<User.id, Socket>
private clients = new Map<User['id'], Socket>();
// key: inviter, value: invitee
private users = new Map<Socket['id'], PublicUserEntity>();
private invite = new Map<User['id'], User['id']>();
private users = new Map<Socket['id'], WsPublicUserEntity>();
// key: requestingUserId, value: requestedUserId
private matchRequests = new Map<User['id'], User['id']>();
private statuses = new Map<User['id'], UserStatus>();

getUser(client: Socket) {
Expand All @@ -58,7 +58,7 @@ export class ChatService {

addClient(user: User, client: Socket) {
this.clients.set(user.id, client);
this.users.set(client.id, new PublicUserEntity(user));
this.users.set(client.id, new WsPublicUserEntity(user));
}

removeClient(client: Socket) {
Expand All @@ -67,20 +67,20 @@ export class ChatService {
this.statuses.delete(user.id);
this.clients.delete(user.id);
this.users.delete(client.id);
this.removeInvite(user.id);
this.removeMatchRequest(user.id);
}
}

addInvite(inviterId: number, inviteeId: number) {
this.invite.set(inviterId, inviteeId);
addMatchRequest(requestingUserId: number, requestedUserId: number) {
this.matchRequests.set(requestingUserId, requestedUserId);
}

getInvite(inviterId: number) {
return this.invite.get(inviterId);
getMatchRequest(requestingUserId: number) {
return this.matchRequests.get(requestingUserId);
}

removeInvite(inviterId: number) {
this.invite.delete(inviterId);
removeMatchRequest(requestingUserId: number) {
this.matchRequests.delete(requestingUserId);
}

addUserToRoom(roomId: number, userId: number) {
Expand Down Expand Up @@ -215,7 +215,7 @@ export class ChatService {
const emitData = {
userId: this.getUserId(client),
status: UserStatus.Offline,
name: this.getUser(client).name,
name: this.getUser(client)?.name,
};
if (emitData.userId) {
client.broadcast.emit('online-status', [emitData]);
Expand Down
Loading

0 comments on commit c5c6495

Please sign in to comment.