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

Fix/backend/strict null check #279

Merged
merged 18 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
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
Loading