diff --git a/libraries/grpc-sdk/src/types/db.ts b/libraries/grpc-sdk/src/types/db.ts index 557dde47d..dd0396c58 100644 --- a/libraries/grpc-sdk/src/types/db.ts +++ b/libraries/grpc-sdk/src/types/db.ts @@ -10,7 +10,8 @@ type operators = | '$regex' | '$options' | '$like' - | '$ilike'; + | '$ilike' + | '$exists'; type arrayOperators = '$in' | '$nin'; type conditionOperators = '$or' | '$and'; @@ -19,6 +20,7 @@ type simpleQuery = { }; type mixedQuery = + | { $exists: boolean } | { [key in operators]?: documentValues } | { [key in arrayOperators]?: documentValues[] }; diff --git a/modules/authorization/src/migrations/actorIndex.migration.ts b/modules/authorization/src/migrations/actorIndex.migration.ts index 86f355620..ea3e082a8 100644 --- a/modules/authorization/src/migrations/actorIndex.migration.ts +++ b/modules/authorization/src/migrations/actorIndex.migration.ts @@ -1,8 +1,8 @@ -import ConduitGrpcSdk, { Query } from '@conduitplatform/grpc-sdk'; +import { Query } from '@conduitplatform/grpc-sdk'; import { ActorIndex } from '../models'; -export const migrateActorIndex = async (grpcSdk: ConduitGrpcSdk) => { - const query: Query = { +export const migrateActorIndex = async () => { + const query: Query = { $or: [ { entityType: '' }, { entityId: '' }, diff --git a/modules/authorization/src/migrations/index.ts b/modules/authorization/src/migrations/index.ts index 8d8a0026a..f39a618af 100644 --- a/modules/authorization/src/migrations/index.ts +++ b/modules/authorization/src/migrations/index.ts @@ -6,9 +6,9 @@ import { migratePermission } from './permission.migration'; export async function runMigrations(grpcSdk: ConduitGrpcSdk) { await Promise.all([ - migrateObjectIndex(grpcSdk), - migrateActorIndex(grpcSdk), - migrateRelationships(grpcSdk), - migratePermission(grpcSdk), + migrateObjectIndex(), + migrateActorIndex(), + migrateRelationships(), + migratePermission(), ]); } diff --git a/modules/authorization/src/migrations/objectIndex.migration.ts b/modules/authorization/src/migrations/objectIndex.migration.ts index 5a1441cf8..9230b216e 100644 --- a/modules/authorization/src/migrations/objectIndex.migration.ts +++ b/modules/authorization/src/migrations/objectIndex.migration.ts @@ -1,8 +1,8 @@ -import ConduitGrpcSdk, { Query } from '@conduitplatform/grpc-sdk'; +import { Query } from '@conduitplatform/grpc-sdk'; import { ObjectIndex } from '../models'; -export const migrateObjectIndex = async (grpcSdk: ConduitGrpcSdk) => { - const query: Query = { +export const migrateObjectIndex = async () => { + const query: Query = { $or: [ { entityType: '' }, { entityId: '' }, diff --git a/modules/authorization/src/migrations/permission.migration.ts b/modules/authorization/src/migrations/permission.migration.ts index 7169a222b..18b60b7b0 100644 --- a/modules/authorization/src/migrations/permission.migration.ts +++ b/modules/authorization/src/migrations/permission.migration.ts @@ -1,8 +1,8 @@ -import ConduitGrpcSdk, { Query } from '@conduitplatform/grpc-sdk'; +import { Query } from '@conduitplatform/grpc-sdk'; import { Permission } from '../models'; -export const migratePermission = async (grpcSdk: ConduitGrpcSdk) => { - const query: Query = { +export const migratePermission = async () => { + const query: Query = { $or: [ { resourceType: '' }, { resourceId: '' }, diff --git a/modules/authorization/src/migrations/relationship.migration.ts b/modules/authorization/src/migrations/relationship.migration.ts index d3fdb1ba0..eb446fb8b 100644 --- a/modules/authorization/src/migrations/relationship.migration.ts +++ b/modules/authorization/src/migrations/relationship.migration.ts @@ -1,8 +1,8 @@ -import ConduitGrpcSdk, { Query } from '@conduitplatform/grpc-sdk'; +import { Query } from '@conduitplatform/grpc-sdk'; import { Relationship } from '../models'; -export const migrateRelationships = async (grpcSdk: ConduitGrpcSdk) => { - const query: Query = { +export const migrateRelationships = async () => { + const query: Query = { $or: [ { resourceType: '' }, { resourceId: '' }, diff --git a/modules/chat/src/config/config.ts b/modules/chat/src/config/config.ts index 4e7eb4c41..1f5ced022 100644 --- a/modules/chat/src/config/config.ts +++ b/modules/chat/src/config/config.ts @@ -16,6 +16,11 @@ export default { format: 'Boolean', default: false, }, + auditMode: { + doc: 'When audit is enabled, deleted rooms and messages are not actually deleted, but marked as deleted', + format: 'Boolean', + default: false, + }, explicit_room_joins: { enabled: { doc: 'Defines whether users should explicitly accept an invitation before being introduced into a chat room', diff --git a/modules/chat/src/migrations/chatMessage.migrate.ts b/modules/chat/src/migrations/chatMessage.migrate.ts new file mode 100644 index 000000000..714e4eb82 --- /dev/null +++ b/modules/chat/src/migrations/chatMessage.migrate.ts @@ -0,0 +1,17 @@ +import { Query } from '@conduitplatform/grpc-sdk'; +import { ChatMessage } from '../models'; + +export const migrateChatMessages = async () => { + const query: Query = { + $or: [{ deleted: '' }, { deleted: { $exists: false } }], + }; + let chatMessages = await ChatMessage.getInstance().findMany(query, undefined, 0, 100); + while (chatMessages.length === 0) { + for (const chatMessage of chatMessages) { + await ChatMessage.getInstance().findByIdAndUpdate(chatMessage._id, { + deleted: false, + }); + } + chatMessages = await ChatMessage.getInstance().findMany(query, undefined, 0, 100); + } +}; diff --git a/modules/chat/src/migrations/chatRoom.migrate.ts b/modules/chat/src/migrations/chatRoom.migrate.ts new file mode 100644 index 000000000..85343e794 --- /dev/null +++ b/modules/chat/src/migrations/chatRoom.migrate.ts @@ -0,0 +1,18 @@ +import { Query } from '@conduitplatform/grpc-sdk'; +import { ChatRoom } from '../models'; + +export const migrateChatRoom = async () => { + const query: Query = { + $or: [{ deleted: '' }, { deleted: { $exists: false } }], + }; + let chatRooms = await ChatRoom.getInstance().findMany(query, undefined, 0, 100); + while (chatRooms.length === 0) { + for (const chatRoom of chatRooms) { + await ChatRoom.getInstance().findByIdAndUpdate(chatRoom._id, { + deleted: false, + participantsLog: [], + }); + } + chatRooms = await ChatRoom.getInstance().findMany(query, undefined, 0, 100); + } +}; diff --git a/modules/chat/src/migrations/index.ts b/modules/chat/src/migrations/index.ts index 801771f5e..a8f8b97f4 100644 --- a/modules/chat/src/migrations/index.ts +++ b/modules/chat/src/migrations/index.ts @@ -1,5 +1,8 @@ import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; +import { migrateChatRoom } from './chatRoom.migrate'; +import { migrateChatMessages } from './chatMessage.migrate'; export async function runMigrations(grpcSdk: ConduitGrpcSdk) { - // ... + await migrateChatRoom(); + await migrateChatMessages(); } diff --git a/modules/chat/src/models/ChatRoom.schema.ts b/modules/chat/src/models/ChatRoom.schema.ts index 3ffbd4439..a24d08f80 100644 --- a/modules/chat/src/models/ChatRoom.schema.ts +++ b/modules/chat/src/models/ChatRoom.schema.ts @@ -15,6 +15,34 @@ const schema: ConduitModel = { required: true, }, ], + creator: { + type: TYPE.Relation, + model: 'User', + }, + participantsLog: [ + { + type: { + action: { + type: TYPE.String, + enum: ['add', 'remove', 'create', 'join', 'leave'], + required: true, + }, + user: { + type: TYPE.Relation, + model: 'User', + required: true, + }, + timestamp: { + type: TYPE.Date, + required: true, + }, + }, + }, + ], + deleted: { + type: TYPE.Boolean, + default: false, + }, createdAt: TYPE.Date, updatedAt: TYPE.Date, }; @@ -33,11 +61,18 @@ const collectionName = undefined; export class ChatRoom extends ConduitActiveSchema { private static _instance: ChatRoom; - _id!: string; - name!: string; - participants!: string[] | User[]; - createdAt!: Date; - updatedAt!: Date; + _id: string; + name: string; + creator?: string | User; + participants: string[] | User[]; + participantsLog: { + action: 'add' | 'remove' | 'create' | 'join' | 'leave'; + user: string | User; + timestamp: Date; + }[]; + deleted: boolean; + createdAt: Date; + updatedAt: Date; private constructor(database: DatabaseProvider) { super(database, ChatRoom.name, schema, modelOptions, collectionName); diff --git a/modules/chat/src/models/Message.schema.ts b/modules/chat/src/models/Message.schema.ts index 39ed988e6..760077cf2 100644 --- a/modules/chat/src/models/Message.schema.ts +++ b/modules/chat/src/models/Message.schema.ts @@ -26,6 +26,10 @@ const schema: ConduitModel = { required: true, }, ], + deleted: { + type: TYPE.Boolean, + default: false, + }, createdAt: TYPE.Date, updatedAt: TYPE.Date, }; @@ -44,13 +48,14 @@ const collectionName = undefined; export class ChatMessage extends ConduitActiveSchema { private static _instance: ChatMessage; - _id!: string; - message!: string; - senderUser!: string | User; - room!: string | ChatRoom; - readBy!: string[] | User[]; - createdAt!: Date; - updatedAt!: Date; + _id: string; + message: string; + senderUser: string | User; + room: string | ChatRoom; + readBy: string[] | User[]; + deleted: boolean; + createdAt: Date; + updatedAt: Date; private constructor(database: DatabaseProvider) { super(database, ChatMessage.name, schema, modelOptions, collectionName); diff --git a/modules/chat/src/routes/index.ts b/modules/chat/src/routes/index.ts index 86c927e25..ec2eb303b 100644 --- a/modules/chat/src/routes/index.ts +++ b/modules/chat/src/routes/index.ts @@ -69,16 +69,19 @@ export class ChatRoutes { } catch (e) { throw new GrpcError(status.INTERNAL, (e as Error).message); } - const roomExists = await ChatRoom.getInstance() - .findOne({ name: roomName }) - .catch((e: Error) => { - throw new GrpcError(status.INTERNAL, e.message); - }); - if (!isNil(roomExists)) { - throw new GrpcError(status.ALREADY_EXISTS, `Room ${roomName} already exists`); - } let room; - const query: Query = { name: roomName, participants: [user._id] }; + const query: Query = { + name: roomName, + creator: user._id, + participants: [user._id], + participantsLog: [ + { + user: user._id, + action: 'create', + timestamp: new Date(new Date().getUTCMilliseconds()), + }, + ], + }; const config = await this.grpcSdk.config.get('chat'); if (config.explicit_room_joins.enabled) { room = await ChatRoom.getInstance() @@ -100,6 +103,18 @@ export class ChatRoutes { }); } else { query['participants'] = Array.from(new Set([user._id, ...users])); + query['participantsLog'] = [ + { + user: user._id, + action: 'create', + timestamp: new Date(new Date().getUTCMilliseconds()), + }, + ...users.map((userId: string) => ({ + user: userId, + action: 'join' as 'join', + timestamp: new Date(new Date().getUTCMilliseconds()), + })), + ]; room = await ChatRoom.getInstance() .create(query) .catch((e: Error) => { @@ -108,7 +123,7 @@ export class ChatRoutes { } this.grpcSdk.bus?.publish( 'chat:create:ChatRoom', - JSON.stringify({ name: roomName, participants: room.participants }), + JSON.stringify({ _id: room._id, name: roomName, participants: room.participants }), ); return { roomId: room._id }; } @@ -129,6 +144,9 @@ export class ChatRoutes { throw new GrpcError(status.INTERNAL, e.message); }, ); + if (room.deleted) { + throw new GrpcError(status.NOT_FOUND, "Room doesn't exist"); + } try { usersToBeAdded = await validateUsersInput(this.grpcSdk, users); } catch (e) { @@ -162,6 +180,14 @@ export class ChatRoutes { await ChatRoom.getInstance() .findByIdAndUpdate(room._id, { participants: Array.from(new Set([...room.participants, ...users])), + participantsLog: [ + ...room.participantsLog, + ...users.map((userId: string) => ({ + user: userId, + action: 'join' as 'join', + timestamp: new Date(new Date().getUTCMilliseconds()), + })), + ], }) .catch((e: Error) => { throw new GrpcError(status.INTERNAL, e.message); @@ -180,15 +206,36 @@ export class ChatRoutes { throw new GrpcError(status.INTERNAL, e.message); }, ); + if (room.deleted) { + throw new GrpcError(status.NOT_FOUND, "Room doesn't exist"); + } const index = room.participants.indexOf(user._id); if (index > -1) { room.participants.splice(index, 1); + room.participantsLog.push({ + user: user._id, + action: 'leave' as 'leave', + timestamp: new Date(new Date().getUTCMilliseconds()), + }); if ( - room.participants.length === 1 && - ConfigController.getInstance().config.deleteEmptyRooms + (room.participants.length === 1 && + ConfigController.getInstance().config.deleteEmptyRooms) || + room.participants.length === 0 ) { - await ChatRoom.getInstance().deleteOne({ _id: room._id }); + if (ConfigController.getInstance().config.auditMode) { + await ChatRoom.getInstance().findByIdAndUpdate(room._id, { + ...room, + deleted: true, + }); + await ChatMessage.getInstance().updateMany( + { room: room._id }, + { deleted: true }, + ); + } else { + await ChatRoom.getInstance().deleteOne({ _id: room._id }); + await ChatMessage.getInstance().deleteMany({ room: room._id }); + } this.grpcSdk.bus?.publish('chat:deleteRoom:ChatRoom', JSON.stringify({ roomId })); } else { await ChatRoom.getInstance() @@ -213,11 +260,14 @@ export class ChatRoutes { let countPromise; if (isNil(roomId)) { const rooms = await ChatRoom.getInstance() - .findMany({ participants: user._id }) + .findMany({ participants: user._id, deleted: false }) .catch((e: Error) => { throw new GrpcError(status.INTERNAL, e.message); }); - const query = { room: { $in: rooms.map((room: ChatRoom) => room._id) } }; + const query = { + room: { $in: rooms.map((room: ChatRoom) => room._id) }, + deleted: false, + }; messagesPromise = ChatMessage.getInstance().findMany( query, undefined, @@ -227,13 +277,19 @@ export class ChatRoutes { ); countPromise = ChatMessage.getInstance().countDocuments(query); } else { - await this.fetchAndValidateRoomById(roomId, user._id).catch((e: Error) => { - throw new GrpcError(status.INTERNAL, e.message); - }); + const room = await this.fetchAndValidateRoomById(roomId, user._id).catch( + (e: Error) => { + throw new GrpcError(status.INTERNAL, e.message); + }, + ); + if (room.deleted) { + throw new GrpcError(status.NOT_FOUND, "Room doesn't exist"); + } messagesPromise = ChatMessage.getInstance() .findMany( { room: roomId, + deleted: false, }, undefined, skip, @@ -243,7 +299,10 @@ export class ChatRoutes { .catch((e: Error) => { throw new GrpcError(status.INTERNAL, e.message); }); - countPromise = ChatMessage.getInstance().countDocuments({ room: roomId }); + countPromise = ChatMessage.getInstance().countDocuments({ + room: roomId, + deleted: false, + }); } const [messages, count] = await Promise.all([messagesPromise, countPromise]).catch( @@ -258,7 +317,7 @@ export class ChatRoutes { const { id } = call.request.params; const { user } = call.request.context; const message = await ChatMessage.getInstance() - .findOne({ _id: id }, undefined, ['room']) + .findOne({ _id: id, deleted: false }, undefined, ['room']) .catch((e: Error) => { throw new GrpcError(status.INTERNAL, e.message); }); @@ -275,12 +334,19 @@ export class ChatRoutes { const { skip, limit, populate } = call.request.params; const { user } = call.request.context; const rooms = await ChatRoom.getInstance() - .findMany({ participants: user._id }, undefined, skip, limit, undefined, populate) + .findMany( + { participants: user._id, deleted: false }, + undefined, + skip, + limit, + undefined, + populate, + ) .catch((e: Error) => { throw new GrpcError(status.INTERNAL, e.message); }); const roomsCount = await ChatRoom.getInstance() - .countDocuments({ participants: user._id }) + .countDocuments({ participants: user._id, deleted: false }) .catch((e: Error) => { throw new GrpcError(status.INTERNAL, e.message); }); @@ -291,7 +357,7 @@ export class ChatRoutes { const { id, populate } = call.request.params; const { user } = call.request.context; const room = await ChatRoom.getInstance() - .findOne({ _id: id, participants: user._id }, undefined, populate) + .findOne({ _id: id, participants: user._id, deleted: false }, undefined, populate) .catch((e: Error) => { throw new GrpcError(status.INTERNAL, e.message); }); @@ -308,7 +374,7 @@ export class ChatRoutes { const { messageId } = call.request.params; const { user } = call.request.context; const message = await ChatMessage.getInstance() - .findOne({ _id: messageId }) + .findOne({ _id: messageId, deleted: false }) .catch((e: Error) => { throw new GrpcError(status.INTERNAL, e.message); }); @@ -318,11 +384,20 @@ export class ChatRoutes { "Message does not exist or you don't have access", ); } - await ChatMessage.getInstance() - .deleteOne({ _id: messageId }) - .catch((e: Error) => { - throw new GrpcError(status.INTERNAL, e.message); - }); + if (ConfigController.getInstance().config.auditMode) { + await ChatMessage.getInstance() + .findByIdAndUpdate(messageId, { deleted: true }) + .catch((e: Error) => { + throw new GrpcError(status.INTERNAL, e.message); + }); + } else { + await ChatMessage.getInstance() + .deleteOne({ _id: messageId }) + .catch((e: Error) => { + throw new GrpcError(status.INTERNAL, e.message); + }); + } + this.grpcSdk.bus?.publish('chat:delete:ChatMessage', JSON.stringify(messageId)); return 'Message deleted successfully'; } @@ -331,7 +406,7 @@ export class ChatRoutes { const { messageId, newMessage } = call.request.params; const { user } = call.request.context; const message: ChatMessage | null = await ChatMessage.getInstance() - .findOne({ _id: messageId }) + .findOne({ _id: messageId, deleted: false }) .catch((e: Error) => { throw new GrpcError(status.INTERNAL, e.message); }); @@ -363,7 +438,7 @@ export class ChatRoutes { async onMessage(call: ParsedSocketRequest): Promise { const { user } = call.request.context; const [roomId, message] = call.request.params; - const room = await ChatRoom.getInstance().findOne({ _id: roomId }); + const room = await ChatRoom.getInstance().findOne({ _id: roomId, deleted: false }); if (isNil(room) || !room.participants.includes(user._id)) { throw new GrpcError( @@ -389,7 +464,7 @@ export class ChatRoutes { async onMessagesRead(call: ParsedSocketRequest): Promise { const user: User = call.request.context.user; const [roomId] = call.request.params; - const room = await ChatRoom.getInstance().findOne({ _id: roomId }); + const room = await ChatRoom.getInstance().findOne({ _id: roomId, deleted: false }); if (isNil(room) || !(room.participants as string[]).includes(user._id)) { throw new GrpcError( status.INVALID_ARGUMENT,