Skip to content

Commit e01c8d7

Browse files
authored
feat: kick user duplicated connections (#42)
1 parent 75dc99d commit e01c8d7

13 files changed

Lines changed: 575 additions & 103 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
3+
module.exports = {
4+
up: async (queryInterface, Sequelize) => {
5+
await queryInterface.addColumn('room_users', 'participant_id', {
6+
type: Sequelize.STRING,
7+
allowNull: true,
8+
});
9+
10+
await queryInterface.addColumn('room_users', 'joined_at', {
11+
type: Sequelize.DATE,
12+
allowNull: true,
13+
});
14+
},
15+
16+
down: async (queryInterface, Sequelize) => {
17+
await queryInterface.removeColumn('room_users', 'participant_id');
18+
await queryInterface.removeColumn('room_users', 'joined_at');
19+
},
20+
};

src/config/configuration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default () => ({
1616
jitsi: {
1717
appId: process.env.JITSI_APP_ID,
1818
apiKey: process.env.JITSI_API_KEY,
19+
apiUrl: process.env.JITSI_API_URL || 'https://8x8.vc',
1920
},
2021
jitsiWebhook: {
2122
secret: process.env.JITSI_WEBHOOK_SECRET,

src/modules/call/call.usecase.spec.ts

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ describe('CallUseCase', () => {
134134
roomService.getRoomByRoomId.mockResolvedValueOnce(roomMock);
135135
roomService.getUserInRoom.mockResolvedValueOnce(null);
136136
roomService.countUsersInRoom.mockResolvedValueOnce(0);
137-
roomService.createUserInRoom.mockResolvedValueOnce(roomUserMock);
137+
roomService.handleUserJoined.mockResolvedValueOnce({
138+
roomUser: roomUserMock,
139+
oldParticipantId: undefined,
140+
});
138141
callService.generateJitsiJWT.mockReturnValueOnce('test-jwt-token');
139142

140143
const result = await callUseCase.joinCall(roomId, userData);
@@ -144,13 +147,15 @@ describe('CallUseCase', () => {
144147
userId,
145148
roomMock.id,
146149
);
147-
expect(roomService.createUserInRoom).toHaveBeenCalledWith({
148-
roomId: roomMock.id,
150+
expect(roomService.handleUserJoined).toHaveBeenCalledWith(
149151
userId,
150-
name: userName,
151-
lastName: userLastName,
152-
anonymous: false,
153-
});
152+
roomMock.id,
153+
{
154+
name: userName,
155+
lastName: userLastName,
156+
anonymous: false,
157+
},
158+
);
154159
expect(result).toEqual({
155160
token: 'test-jwt-token',
156161
room: roomId,
@@ -168,7 +173,10 @@ describe('CallUseCase', () => {
168173
roomService.getRoomByRoomId.mockResolvedValueOnce(roomMock);
169174
roomService.getUserInRoom.mockResolvedValueOnce(null);
170175
roomService.countUsersInRoom.mockResolvedValueOnce(0);
171-
roomService.createUserInRoom.mockResolvedValueOnce(anonymousUserMock);
176+
roomService.handleUserJoined.mockResolvedValueOnce({
177+
roomUser: anonymousUserMock,
178+
oldParticipantId: undefined,
179+
});
172180
callService.generateJitsiJWT.mockReturnValueOnce('test-jwt-token');
173181

174182
const result = await callUseCase.joinCall(roomId, userData);
@@ -178,10 +186,10 @@ describe('CallUseCase', () => {
178186
autoGeneratedUUID,
179187
roomMock.id,
180188
);
181-
expect(roomService.createUserInRoom).toHaveBeenCalledWith(
189+
expect(roomService.handleUserJoined).toHaveBeenCalledWith(
190+
autoGeneratedUUID,
191+
roomMock.id,
182192
expect.objectContaining({
183-
roomId: roomMock.id,
184-
userId: autoGeneratedUUID,
185193
name: userName,
186194
anonymous: true,
187195
}),
@@ -213,9 +221,10 @@ describe('CallUseCase', () => {
213221
roomService.getRoomByRoomId.mockResolvedValueOnce(roomMock);
214222
roomService.getUserInRoom.mockResolvedValueOnce(null);
215223
roomService.countUsersInRoom.mockResolvedValueOnce(0);
216-
roomService.createUserInRoom.mockResolvedValueOnce(
217-
customAnonymousUserMock,
218-
);
224+
roomService.handleUserJoined.mockResolvedValueOnce({
225+
roomUser: customAnonymousUserMock,
226+
oldParticipantId: undefined,
227+
});
219228
callService.generateJitsiJWT.mockReturnValueOnce('test-jwt-token');
220229

221230
const result = await callUseCase.joinCall(roomId, userData);
@@ -225,10 +234,10 @@ describe('CallUseCase', () => {
225234
customAnonymousId,
226235
roomMock.id,
227236
);
228-
expect(roomService.createUserInRoom).toHaveBeenCalledWith(
237+
expect(roomService.handleUserJoined).toHaveBeenCalledWith(
238+
customAnonymousId,
239+
roomMock.id,
229240
expect.objectContaining({
230-
roomId: roomMock.id,
231-
userId: customAnonymousId,
232241
name: userName,
233242
anonymous: true,
234243
}),
@@ -248,7 +257,10 @@ describe('CallUseCase', () => {
248257
roomService.getRoomByRoomId.mockResolvedValueOnce(closedRoomMock);
249258
roomService.getUserInRoom.mockResolvedValueOnce(null);
250259
roomService.countUsersInRoom.mockResolvedValueOnce(0);
251-
roomService.createUserInRoom.mockResolvedValueOnce(roomUserMock);
260+
roomService.handleUserJoined.mockResolvedValueOnce({
261+
roomUser: roomUserMock,
262+
oldParticipantId: undefined,
263+
});
252264
callService.generateJitsiJWT.mockReturnValueOnce('test-jwt-token');
253265
roomService.openRoom.mockResolvedValueOnce();
254266

@@ -314,6 +326,10 @@ describe('CallUseCase', () => {
314326
roomService.getRoomByRoomId.mockResolvedValueOnce(openRoomMock);
315327
roomService.getUserInRoom.mockResolvedValueOnce(roomUserMock);
316328
roomService.countUsersInRoom.mockResolvedValueOnce(1);
329+
roomService.handleUserJoined.mockResolvedValueOnce({
330+
roomUser: roomUserMock,
331+
oldParticipantId: undefined,
332+
});
317333
callService.generateJitsiJWT.mockReturnValueOnce('test-jwt-token');
318334

319335
const result = await callUseCase.joinCall(roomId, userData);
@@ -322,7 +338,15 @@ describe('CallUseCase', () => {
322338
userId,
323339
openRoomMock.id,
324340
);
325-
expect(roomService.createUserInRoom).not.toHaveBeenCalled();
341+
expect(roomService.handleUserJoined).toHaveBeenCalledWith(
342+
userId,
343+
openRoomMock.id,
344+
{
345+
name: undefined,
346+
lastName: undefined,
347+
anonymous: false,
348+
},
349+
);
326350
expect(result.userId).toEqual(roomUserMock.userId);
327351
});
328352
});
@@ -350,7 +374,10 @@ describe('CallUseCase', () => {
350374

351375
roomService.getUserInRoom.mockResolvedValueOnce(null);
352376
roomService.countUsersInRoom.mockResolvedValueOnce(0);
353-
roomService.createUserInRoom.mockResolvedValueOnce(registeredRoomUser);
377+
roomService.handleUserJoined.mockResolvedValueOnce({
378+
roomUser: registeredRoomUser,
379+
oldParticipantId: undefined,
380+
});
354381

355382
await callUseCase.joinCall(roomId, {
356383
userId,
@@ -380,18 +407,21 @@ describe('CallUseCase', () => {
380407

381408
roomService.getUserInRoom.mockResolvedValueOnce(null);
382409
roomService.countUsersInRoom.mockResolvedValueOnce(0);
383-
roomService.createUserInRoom.mockResolvedValueOnce(anonymousRoomUser);
410+
roomService.handleUserJoined.mockResolvedValueOnce({
411+
roomUser: anonymousRoomUser,
412+
oldParticipantId: undefined,
413+
});
384414
callService.generateJitsiJWT.mockReturnValueOnce('test-jwt-token');
385415

386416
await callUseCase.joinCall(roomId, {
387417
name,
388418
anonymous: true,
389419
});
390420

391-
expect(roomService.createUserInRoom).toHaveBeenCalledWith(
421+
expect(roomService.handleUserJoined).toHaveBeenCalledWith(
422+
autoGeneratedUUID,
423+
openRoomMock.id,
392424
expect.objectContaining({
393-
roomId: openRoomMock.id,
394-
userId: autoGeneratedUUID,
395425
name,
396426
anonymous: true,
397427
}),
@@ -418,17 +448,20 @@ describe('CallUseCase', () => {
418448

419449
roomService.getUserInRoom.mockResolvedValueOnce(null);
420450
roomService.countUsersInRoom.mockResolvedValueOnce(0);
421-
roomService.createUserInRoom.mockResolvedValueOnce(userWithoutId);
451+
roomService.handleUserJoined.mockResolvedValueOnce({
452+
roomUser: userWithoutId,
453+
oldParticipantId: undefined,
454+
});
422455
callService.generateJitsiJWT.mockReturnValueOnce('test-jwt-token');
423456

424457
await callUseCase.joinCall(roomId, {
425458
name,
426459
});
427460

428-
expect(roomService.createUserInRoom).toHaveBeenCalledWith(
461+
expect(roomService.handleUserJoined).toHaveBeenCalledWith(
462+
undefined,
463+
openRoomMock.id,
429464
expect.objectContaining({
430-
roomId: openRoomMock.id,
431-
userId: undefined,
432465
name,
433466
anonymous: true,
434467
}),

src/modules/call/call.usecase.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,28 +101,35 @@ export class CallUseCase {
101101
throw new ForbiddenException('Room is closed');
102102
}
103103

104+
const currentUsersCount = await this.roomService.countUsersInRoom(room.id);
104105
const existentUserInRoom = await this.roomService.getUserInRoom(
105106
joiningUserData.userId,
106107
room.id,
107108
);
108109

109-
const currentUsersCount = await this.roomService.countUsersInRoom(room.id);
110-
111110
const isRoomFull = currentUsersCount >= room.maxUsersAllowed;
112-
113111
if (isRoomFull && !existentUserInRoom) {
114112
throw new BadRequestException('The room is full');
115113
}
116114

117-
const roomUser =
118-
existentUserInRoom ??
119-
(await this.roomService.createUserInRoom({
120-
roomId: room.id,
121-
userId: joiningUserData?.userId,
122-
name: joiningUserData?.name,
123-
lastName: joiningUserData?.lastName,
124-
anonymous: Boolean(joiningUserData?.anonymous),
125-
}));
115+
const { roomUser, oldParticipantId } =
116+
await this.roomService.handleUserJoined(joiningUserData.userId, room.id, {
117+
name: joiningUserData.name,
118+
lastName: joiningUserData.lastName,
119+
anonymous: joiningUserData.anonymous,
120+
});
121+
122+
if (oldParticipantId) {
123+
this.logger.log(
124+
{
125+
roomId,
126+
userId: joiningUserData.userId,
127+
oldParticipantId,
128+
},
129+
'Kicking existing participant on new join',
130+
);
131+
await this.callService.kickParticipant(roomId, oldParticipantId);
132+
}
126133

127134
const tokenData = this.callService.generateJitsiJWT(
128135
{

src/modules/call/domain/room-user.domain.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ export interface RoomUserAttributes {
22
id: string;
33
roomId: string;
44
userId: string;
5+
participantId?: string;
6+
joinedAt?: Date;
57
name?: string;
68
lastName?: string;
79
anonymous: boolean;
@@ -13,6 +15,8 @@ export class RoomUser implements RoomUserAttributes {
1315
id: string;
1416
roomId: string;
1517
userId: string;
18+
participantId?: string;
19+
joinedAt?: Date;
1620
name?: string;
1721
lastName?: string;
1822
anonymous: boolean;
@@ -28,6 +32,8 @@ export class RoomUser implements RoomUserAttributes {
2832
id: this.id,
2933
roomId: this.roomId,
3034
userId: this.userId,
35+
participantId: this.participantId,
36+
joinedAt: this.joinedAt,
3137
name: this.name,
3238
lastName: this.lastName,
3339
anonymous: this.anonymous,
@@ -41,6 +47,8 @@ export class RoomUser implements RoomUserAttributes {
4147
id: attributes.id,
4248
roomId: attributes.roomId,
4349
userId: attributes.userId,
50+
participantId: attributes.participantId,
51+
joinedAt: attributes.joinedAt,
4452
name: attributes.name,
4553
lastName: attributes.lastName,
4654
anonymous: attributes.anonymous,

src/modules/call/infrastructure/room-user.repository.spec.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('SequelizeRoomUserRepository', () => {
5757

5858
const result = await repository.create(roomUserCreateData);
5959

60-
expect(createRoomUserSpy).toHaveBeenCalledWith(roomUserCreateData);
60+
expect(createRoomUserSpy).toHaveBeenCalledWith(roomUserCreateData, { transaction: undefined });
6161
expect(result).toBeInstanceOf(RoomUser);
6262
expect(result.id).toEqual(mockRoomUserData.id);
6363
expect(result.roomId).toEqual(mockRoomUserData.roomId);
@@ -69,24 +69,33 @@ describe('SequelizeRoomUserRepository', () => {
6969
const mockRoomUser = createMock<RoomUserModel>(mockRoomUserData);
7070

7171
const findRoomUserByIdSpy = jest
72-
.spyOn(roomUserModel, 'findByPk')
72+
.spyOn(roomUserModel, 'findOne')
7373
.mockResolvedValueOnce(mockRoomUser);
7474

75-
const result = await repository.findById(1);
75+
const result = await repository.findById(mockRoomUserData.id);
7676

77-
expect(findRoomUserByIdSpy).toHaveBeenCalledWith(1);
77+
expect(findRoomUserByIdSpy).toHaveBeenCalledWith({
78+
where: { id: mockRoomUserData.id },
79+
lock: undefined,
80+
transaction: undefined,
81+
});
7882
expect(result).toBeInstanceOf(RoomUser);
7983
expect(result?.id).toEqual(mockRoomUserData.id);
8084
});
8185

8286
it('When the room user does not exist in the DB, then null is returned', async () => {
8387
const findRoomUserByIdSpy = jest
84-
.spyOn(roomUserModel, 'findByPk')
88+
.spyOn(roomUserModel, 'findOne')
8589
.mockResolvedValueOnce(null);
8690

87-
const result = await repository.findById(999);
91+
const nonExistentId = v4();
92+
const result = await repository.findById(nonExistentId);
8893

89-
expect(findRoomUserByIdSpy).toHaveBeenCalledWith(999);
94+
expect(findRoomUserByIdSpy).toHaveBeenCalledWith({
95+
where: { id: nonExistentId },
96+
lock: undefined,
97+
transaction: undefined,
98+
});
9099
expect(result).toBeNull();
91100
});
92101
});
@@ -180,11 +189,11 @@ describe('SequelizeRoomUserRepository', () => {
180189
.spyOn(roomUserModel, 'update')
181190
.mockResolvedValueOnce([1]);
182191

183-
await repository.update(1, { name: 'Updated Name' });
192+
await repository.update(mockRoomUserData.id, { name: 'Updated Name' });
184193

185194
expect(updateRoomUserSpy).toHaveBeenCalledWith(
186195
{ name: 'Updated Name' },
187-
{ where: { id: 1 } },
196+
{ where: { id: mockRoomUserData.id } },
188197
);
189198
});
190199
});

0 commit comments

Comments
 (0)