Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 35 additions & 8 deletions api/src/chat/services/message.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
import { buildTestingMocks } from '@/utils/test/utils';
import { IOOutgoingSubscribeMessage } from '@/websocket/pipes/io-message.pipe';
import { Room } from '@/websocket/types';
import { SocketRequest } from '@/websocket/utils/socket-request';
import { SocketResponse } from '@/websocket/utils/socket-response';
import { WebsocketGateway } from '@/websocket/websocket.gateway';

import { MessageRepository } from '../repositories/message.repository';
Expand Down Expand Up @@ -50,9 +52,14 @@ describe('MessageService', () => {
success: true,
subscribe: Room.MESSAGE,
};
let buildReqRes: (
method: 'GET' | 'POST',
subscriberId: string,
) => [SocketRequest, SocketResponse];

beforeAll(async () => {
const { getMocks } = await buildTestingMocks({
models: ['RoleModel', 'ModelModel'],
autoInjectFrom: ['providers'],
imports: [rootMongooseTestModule(installMessageFixtures)],
providers: [MessageService, SubscriberRepository, UserRepository],
Expand Down Expand Up @@ -81,24 +88,44 @@ describe('MessageService', () => {
joinNotificationSockets: jest.fn(),
};
mockMessageService = new MessageService({} as any, mockGateway as any);
buildReqRes = (method: 'GET' | 'POST', userId: string) => [
{
sessionID: SESSION_ID,
method,
session: { passport: { user: { id: userId } } },
} as SocketRequest,
{
json: jest.fn(),
status: jest.fn().mockReturnThis(),
} as any,
];
});

afterEach(jest.clearAllMocks);
afterAll(closeInMongodConnection);

describe('subscribe', () => {
it('should join Notification sockets message room and return a success response', async () => {
const req = { sessionID: SESSION_ID };
const res = {
json: jest.fn(),
status: jest.fn().mockReturnThis(),
};
it('should join Notification sockets GET message room and return a success response', async () => {
const [req, res] = buildReqRes('GET', allUsers[0].id);
await mockMessageService.subscribe(req, res);

expect(mockGateway.joinNotificationSockets).toHaveBeenCalledWith(
req,
Room.MESSAGE,
'message',
);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(SUCCESS_PAYLOAD);
});

await mockMessageService.subscribe(req as any, res as any);
it('should join Notification sockets POST message room and return a success response', async () => {
const [req, res] = buildReqRes('POST', allUsers[0].id);
await mockMessageService.subscribe(req, res);

expect(mockGateway.joinNotificationSockets).toHaveBeenCalledWith(
SESSION_ID,
req,
Room.MESSAGE,
'message',
);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(SUCCESS_PAYLOAD);
Expand Down
8 changes: 4 additions & 4 deletions api/src/chat/services/message.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ export class MessageService extends BaseService<
@SocketRes() res: SocketResponse,
): Promise<IOOutgoingSubscribeMessage> {
try {
await this.gateway.joinNotificationSockets(req.sessionID, Room.MESSAGE);
await this.gateway.joinNotificationSockets(req, Room.MESSAGE, 'message');

return res.status(200).json({
success: true,
subscribe: Room.MESSAGE,
});
} catch (e) {
this.logger.error('Websocket subscription', e);
throw new InternalServerErrorException(e);
} catch (err) {
this.logger.error('Websocket message room subscription error', err);
throw new InternalServerErrorException(err);
}
}

Expand Down
45 changes: 36 additions & 9 deletions api/src/chat/services/subscriber.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
import { buildTestingMocks } from '@/utils/test/utils';
import { IOOutgoingSubscribeMessage } from '@/websocket/pipes/io-message.pipe';
import { Room } from '@/websocket/types';
import { SocketRequest } from '@/websocket/utils/socket-request';
import { SocketResponse } from '@/websocket/utils/socket-response';
import { WebsocketGateway } from '@/websocket/websocket.gateway';

import { LabelRepository } from '../repositories/label.repository';
Expand Down Expand Up @@ -55,9 +57,14 @@ describe('SubscriberService', () => {
success: true,
subscribe: Room.SUBSCRIBER,
};
let buildReqRes: (
method: 'GET' | 'POST',
subscriberId: string,
) => [SocketRequest, SocketResponse];

beforeAll(async () => {
const { getMocks } = await buildTestingMocks({
models: ['RoleModel', 'ModelModel'],
autoInjectFrom: ['providers'],
imports: [rootMongooseTestModule(installSubscriberFixtures)],
providers: [SubscriberService, LabelRepository, UserRepository],
Expand All @@ -82,28 +89,48 @@ describe('SubscriberService', () => {
joinNotificationSockets: jest.fn(),
};
mockSubscriberService = new SubscriberService(
{} as any,
subscriberRepository,
{} as any,
mockGateway as any,
);
buildReqRes = (method: 'GET' | 'POST', userId: string) => [
{
sessionID: SESSION_ID,
method,
session: { passport: { user: { id: userId } } },
} as SocketRequest,
{
json: jest.fn(),
status: jest.fn().mockReturnThis(),
} as any,
];
});

afterEach(jest.clearAllMocks);
afterAll(closeInMongodConnection);

describe('subscribe', () => {
it('should join Notification sockets subscriber room and return a success response', async () => {
const req = { sessionID: SESSION_ID };
const res = {
json: jest.fn(),
status: jest.fn().mockReturnThis(),
};
it('should join Notification sockets GET subscriber room and return a success response', async () => {
const [req, res] = buildReqRes('GET', allUsers[0].id);
await mockSubscriberService.subscribe(req, res);

expect(mockGateway.joinNotificationSockets).toHaveBeenCalledWith(
req,
Room.SUBSCRIBER,
'subscriber',
);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(SUCCESS_PAYLOAD);
});

await mockSubscriberService.subscribe(req as any, res as any);
it('should join Notification sockets POST subscriber room and return a success response', async () => {
const [req, res] = buildReqRes('POST', allUsers[0].id);
await mockSubscriberService.subscribe(req, res);

expect(mockGateway.joinNotificationSockets).toHaveBeenCalledWith(
SESSION_ID,
req,
Room.SUBSCRIBER,
'subscriber',
);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(SUCCESS_PAYLOAD);
Expand Down
9 changes: 5 additions & 4 deletions api/src/chat/services/subscriber.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,18 @@ export class SubscriberService extends BaseService<
): Promise<IOOutgoingSubscribeMessage> {
try {
await this.gateway.joinNotificationSockets(
req.sessionID,
req,
Room.SUBSCRIBER,
'subscriber',
);

return res.status(200).json({
success: true,
subscribe: Room.SUBSCRIBER,
});
} catch (e) {
this.logger.error('Websocket subscription');
throw new InternalServerErrorException(e);
} catch (err) {
this.logger.error('Websocket subscriber room subscription error', err);
throw new InternalServerErrorException(err);
}
}

Expand Down
41 changes: 40 additions & 1 deletion api/src/user/services/permission.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
Expand All @@ -22,6 +22,8 @@ import {
PermissionFull,
PermissionPopulate,
} from '../schemas/permission.schema';
import { MethodToAction } from '../types/action.type';
import { TModel } from '../types/model.type';
import { PermissionsTree } from '../types/permission.type';

@Injectable()
Expand Down Expand Up @@ -80,4 +82,41 @@ export class PermissionService extends BaseService<
return acc;
}, {});
}

/**
* Checks if a request (REST/WebSocket) is authorized to get access
*
* @param method - The Request Method
* @param userRoles - An array of ID's user Roles or empty
* @param targetModel - The target model that we want access
* @returns
*/
async canAccess(
method: string,
userRoles: string[],
targetModel: TModel,
): Promise<boolean> {
try {
const permissions = await this.getPermissions();

if (permissions && userRoles.length) {
const permissionsFromRoles = Object.entries(permissions)
.filter(([key, _]) => userRoles.includes(key))
.map(([_, value]) => value);

if (
permissionsFromRoles.some((permission) =>
permission[targetModel]?.includes(MethodToAction[method]),
)
) {
return true;
}
}
} catch (e) {
this.logger.error('Request has no ability to get access', e);
return false;
}

return false;
}
}
15 changes: 13 additions & 2 deletions api/src/utils/test/fixtures/model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
Expand All @@ -18,13 +18,24 @@ export const modelFixtures: ModelCreateDto[] = [
attributes: { att: 'att' },
relation: 'role',
},

{
name: 'Content',
identity: 'content',
attributes: { att: 'att' },
relation: 'role',
},
{
name: 'Message',
identity: 'message',
attributes: { att: 'att' },
relation: 'role',
},
{
name: 'Subscriber',
identity: 'subscriber',
attributes: { att: 'att' },
relation: 'role',
},
];

export const installModelFixtures = async () => {
Expand Down
26 changes: 25 additions & 1 deletion api/src/utils/test/fixtures/permission.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
Expand Down Expand Up @@ -40,6 +40,30 @@ export const permissionFixtures: PermissionCreateDto[] = [
role: '0',
relation: 'role',
},
{
model: '2',
action: Action.CREATE,
role: '1',
relation: 'role',
},
{
model: '2',
action: Action.READ,
role: '1',
relation: 'role',
},
{
model: '3',
action: Action.CREATE,
role: '1',
relation: 'role',
},
{
model: '3',
action: Action.READ,
role: '1',
relation: 'role',
},
];

export const installPermissionFixtures = async () => {
Expand Down
4 changes: 2 additions & 2 deletions api/src/utils/test/fixtures/subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { getFixturesWithDefaultValues } from '../defaultValues';
import { FixturesTypeBuilder } from '../types';

import { installLabelFixtures } from './label';
import { installUserFixtures } from './user';
import { installPermissionFixtures } from './permission';

type TSubscriberFixtures = FixturesTypeBuilder<Subscriber, SubscriberCreateDto>;

Expand Down Expand Up @@ -106,7 +106,7 @@ export const installSubscriberFixtures = async () => {
SubscriberModel.name,
SubscriberModel.schema,
);
const { users } = await installUserFixtures();
const { users } = await installPermissionFixtures();
const labels = await installLabelFixtures();
const subscribers = await Subscriber.insertMany(
subscriberFixtures.map((subscriberFixture) => ({
Expand Down
Loading