feat: Security
This commit is contained in:
@@ -55,6 +55,8 @@ import { hydrateMessages } from './messages.helpers';
|
||||
import { canEditMessage } from '../../domains/chat/domain/rules/message.rules';
|
||||
import { resolveRoomPermission } from '../../domains/access-control';
|
||||
import { dispatchIncomingMessage, IncomingMessageContext } from './messages-incoming.handlers';
|
||||
import { MessageRevisionService } from '../../domains/chat/application/services/message-revision.service';
|
||||
import { materializeMessageFromRevision } from '../../domains/chat/domain/rules/message-revision.builder.rules';
|
||||
|
||||
const INITIAL_ROOM_MESSAGE_LIMIT = 30;
|
||||
/** Cap on simultaneous browser-cache prefetches for apps with many saved rooms. */
|
||||
@@ -73,6 +75,7 @@ export class MessagesEffects {
|
||||
private readonly linkMetadata = inject(LinkMetadataService);
|
||||
private readonly platform = inject(PlatformService);
|
||||
private readonly i18n = inject(AppI18nService);
|
||||
private readonly messageRevisions = inject(MessageRevisionService);
|
||||
|
||||
/** Loads messages for a room from the local database, hydrating reactions. */
|
||||
loadMessages$ = createEffect(() =>
|
||||
@@ -235,7 +238,7 @@ export class MessagesEffects {
|
||||
return of(MessagesActions.sendMessageFailure({ error: this.i18n.instant('chat.effects.notConnectedToRoom') }));
|
||||
}
|
||||
|
||||
const message: Message = {
|
||||
const draftMessage: Message = {
|
||||
id: uuidv4(),
|
||||
roomId: currentRoom.id,
|
||||
channelId: channelId || 'general',
|
||||
@@ -245,27 +248,44 @@ export class MessagesEffects {
|
||||
timestamp: this.timeSync.now(),
|
||||
reactions: [],
|
||||
isDeleted: false,
|
||||
replyToId
|
||||
replyToId,
|
||||
revision: 0
|
||||
};
|
||||
|
||||
this.attachments.rememberMessageRoom(message.id, message.roomId);
|
||||
return from((async () => {
|
||||
const revision = await this.messageRevisions.createSignedRevision({
|
||||
message: draftMessage,
|
||||
type: 'create',
|
||||
actorId: currentUser.id,
|
||||
editedAt: draftMessage.timestamp
|
||||
});
|
||||
const message = materializeMessageFromRevision(null, revision);
|
||||
|
||||
this.trackBackgroundOperation(
|
||||
this.db.saveMessage(message),
|
||||
'Failed to persist outgoing chat message',
|
||||
{
|
||||
channelId: message.channelId,
|
||||
contentLength: message.content.length,
|
||||
messageId: message.id,
|
||||
roomId: message.roomId
|
||||
}
|
||||
);
|
||||
this.attachments.rememberMessageRoom(message.id, message.roomId);
|
||||
|
||||
this.customEmoji.pushEmojisInContent(content);
|
||||
this.webrtc.broadcastMessage({ type: 'chat-message',
|
||||
message });
|
||||
this.trackBackgroundOperation(
|
||||
this.db.saveMessage(message),
|
||||
'Failed to persist outgoing chat message',
|
||||
{
|
||||
channelId: message.channelId,
|
||||
contentLength: message.content.length,
|
||||
messageId: message.id,
|
||||
roomId: message.roomId
|
||||
}
|
||||
);
|
||||
|
||||
return of(MessagesActions.sendMessageSuccess({ message }));
|
||||
this.trackBackgroundOperation(
|
||||
this.messageRevisions.persistRevision(revision),
|
||||
'Failed to persist outgoing message revision',
|
||||
{ messageId: message.id, revision: revision.revision }
|
||||
);
|
||||
|
||||
this.customEmoji.pushEmojisInContent(content);
|
||||
this.webrtc.broadcastMessage({ type: 'chat-message', message });
|
||||
this.messageRevisions.broadcastRevision(revision);
|
||||
|
||||
return MessagesActions.sendMessageSuccess({ message });
|
||||
})());
|
||||
}),
|
||||
catchError((error) =>
|
||||
of(MessagesActions.sendMessageFailure({ error: error.message }))
|
||||
@@ -295,26 +315,38 @@ export class MessagesEffects {
|
||||
|
||||
const editedAt = this.timeSync.now();
|
||||
|
||||
this.trackBackgroundOperation(
|
||||
this.db.updateMessage(messageId, { content,
|
||||
editedAt }),
|
||||
'Failed to persist edited chat message',
|
||||
{
|
||||
contentLength: content.length,
|
||||
editedAt,
|
||||
messageId
|
||||
}
|
||||
);
|
||||
return from((async () => {
|
||||
const revision = await this.messageRevisions.createSignedRevision({
|
||||
message: existing,
|
||||
type: 'author-edit',
|
||||
actorId: currentUser.id,
|
||||
content,
|
||||
editedAt
|
||||
});
|
||||
const updatedMessage = materializeMessageFromRevision(existing, revision);
|
||||
|
||||
this.customEmoji.pushEmojisInContent(content);
|
||||
this.webrtc.broadcastMessage({ type: 'message-edited',
|
||||
messageId,
|
||||
content,
|
||||
editedAt });
|
||||
this.trackBackgroundOperation(
|
||||
this.db.saveMessage(updatedMessage),
|
||||
'Failed to persist edited chat message',
|
||||
{
|
||||
contentLength: content.length,
|
||||
editedAt,
|
||||
messageId
|
||||
}
|
||||
);
|
||||
|
||||
return of(MessagesActions.editMessageSuccess({ messageId,
|
||||
content,
|
||||
editedAt }));
|
||||
this.trackBackgroundOperation(
|
||||
this.messageRevisions.persistRevision(revision),
|
||||
'Failed to persist edited message revision',
|
||||
{ messageId, revision: revision.revision }
|
||||
);
|
||||
|
||||
this.customEmoji.pushEmojisInContent(content);
|
||||
this.webrtc.broadcastMessage({ type: 'message-edited', messageId, content, editedAt });
|
||||
this.messageRevisions.broadcastRevision(revision);
|
||||
|
||||
return MessagesActions.editMessageSuccess({ messageId, content, editedAt });
|
||||
})());
|
||||
}),
|
||||
catchError((error) =>
|
||||
of(MessagesActions.editMessageFailure({ error: error.message }))
|
||||
@@ -346,30 +378,39 @@ export class MessagesEffects {
|
||||
|
||||
const deletedAt = this.timeSync.now();
|
||||
|
||||
this.trackBackgroundOperation(
|
||||
this.db.updateMessage(messageId, {
|
||||
content: DELETED_MESSAGE_CONTENT,
|
||||
return from((async () => {
|
||||
const revision = await this.messageRevisions.createSignedRevision({
|
||||
message: existing,
|
||||
type: 'author-delete',
|
||||
actorId: currentUser.id,
|
||||
editedAt: deletedAt,
|
||||
isDeleted: true
|
||||
}),
|
||||
'Failed to persist message deletion',
|
||||
{
|
||||
deletedAt,
|
||||
messageId
|
||||
}
|
||||
);
|
||||
});
|
||||
const deletedMessage = materializeMessageFromRevision(existing, revision);
|
||||
|
||||
this.trackBackgroundOperation(
|
||||
this.attachments.deleteForMessage(messageId),
|
||||
'Failed to delete message attachments',
|
||||
{ messageId }
|
||||
);
|
||||
this.trackBackgroundOperation(
|
||||
this.db.saveMessage(deletedMessage),
|
||||
'Failed to persist message deletion',
|
||||
{ deletedAt, messageId }
|
||||
);
|
||||
|
||||
this.webrtc.broadcastMessage({ type: 'message-deleted',
|
||||
messageId,
|
||||
deletedAt });
|
||||
this.trackBackgroundOperation(
|
||||
this.messageRevisions.persistRevision(revision),
|
||||
'Failed to persist deleted message revision',
|
||||
{ messageId, revision: revision.revision }
|
||||
);
|
||||
|
||||
return of(MessagesActions.deleteMessageSuccess({ messageId }));
|
||||
this.trackBackgroundOperation(
|
||||
this.attachments.deleteForMessage(messageId),
|
||||
'Failed to delete message attachments',
|
||||
{ messageId }
|
||||
);
|
||||
|
||||
this.webrtc.broadcastMessage({ type: 'message-deleted', messageId, deletedAt });
|
||||
this.messageRevisions.broadcastRevision(revision);
|
||||
|
||||
return MessagesActions.deleteMessageSuccess({ messageId });
|
||||
})());
|
||||
}),
|
||||
catchError((error) =>
|
||||
of(MessagesActions.deleteMessageFailure({ error: error.message }))
|
||||
@@ -399,37 +440,54 @@ export class MessagesEffects {
|
||||
return of(MessagesActions.deleteMessageFailure({ error: this.i18n.instant('chat.effects.permissionDenied') }));
|
||||
}
|
||||
|
||||
const deletedAt = this.timeSync.now();
|
||||
return from(this.db.getMessageById(messageId)).pipe(
|
||||
mergeMap((existing) => {
|
||||
if (!existing) {
|
||||
return of(MessagesActions.deleteMessageFailure({ error: this.i18n.instant('chat.effects.messageNotFound') }));
|
||||
}
|
||||
|
||||
this.trackBackgroundOperation(
|
||||
this.db.updateMessage(messageId, {
|
||||
content: DELETED_MESSAGE_CONTENT,
|
||||
editedAt: deletedAt,
|
||||
isDeleted: true
|
||||
}),
|
||||
'Failed to persist admin message deletion',
|
||||
{
|
||||
deletedBy: currentUser.id,
|
||||
deletedAt,
|
||||
messageId
|
||||
}
|
||||
const deletedAt = this.timeSync.now();
|
||||
|
||||
return from((async () => {
|
||||
const revision = await this.messageRevisions.createSignedRevision({
|
||||
message: existing,
|
||||
type: 'moderate-delete',
|
||||
actorId: currentUser.id,
|
||||
editedAt: deletedAt,
|
||||
isDeleted: true
|
||||
});
|
||||
const deletedMessage = materializeMessageFromRevision(existing, revision);
|
||||
|
||||
this.trackBackgroundOperation(
|
||||
this.db.saveMessage(deletedMessage),
|
||||
'Failed to persist admin message deletion',
|
||||
{ deletedBy: currentUser.id, deletedAt, messageId }
|
||||
);
|
||||
|
||||
this.trackBackgroundOperation(
|
||||
this.messageRevisions.persistRevision(revision),
|
||||
'Failed to persist moderated delete revision',
|
||||
{ messageId, revision: revision.revision }
|
||||
);
|
||||
|
||||
this.trackBackgroundOperation(
|
||||
this.attachments.deleteForMessage(messageId),
|
||||
'Failed to delete admin-deleted message attachments',
|
||||
{ deletedBy: currentUser.id, messageId }
|
||||
);
|
||||
|
||||
this.webrtc.broadcastMessage({
|
||||
type: 'message-deleted',
|
||||
messageId,
|
||||
deletedBy: currentUser.id,
|
||||
deletedAt
|
||||
});
|
||||
this.messageRevisions.broadcastRevision(revision);
|
||||
|
||||
return MessagesActions.deleteMessageSuccess({ messageId });
|
||||
})());
|
||||
})
|
||||
);
|
||||
|
||||
this.trackBackgroundOperation(
|
||||
this.attachments.deleteForMessage(messageId),
|
||||
'Failed to delete admin-deleted message attachments',
|
||||
{
|
||||
deletedBy: currentUser.id,
|
||||
messageId
|
||||
}
|
||||
);
|
||||
|
||||
this.webrtc.broadcastMessage({ type: 'message-deleted',
|
||||
messageId,
|
||||
deletedBy: currentUser.id,
|
||||
deletedAt });
|
||||
|
||||
return of(MessagesActions.deleteMessageSuccess({ messageId }));
|
||||
}),
|
||||
catchError((error) =>
|
||||
of(MessagesActions.deleteMessageFailure({ error: error.message }))
|
||||
@@ -606,12 +664,13 @@ export class MessagesEffects {
|
||||
webrtc: this.webrtc,
|
||||
attachments: this.attachments,
|
||||
debugging: this.debugging,
|
||||
messageRevisions: this.messageRevisions,
|
||||
currentUser: currentUser ?? null,
|
||||
currentRoom,
|
||||
savedRooms
|
||||
};
|
||||
|
||||
return dispatchIncomingMessage(event, ctx).pipe(
|
||||
return dispatchIncomingMessage(event as Parameters<typeof dispatchIncomingMessage>[0], ctx).pipe(
|
||||
catchError((error) => {
|
||||
const eventRecord = event as unknown as Record<string, unknown>;
|
||||
const messageRecord = (eventRecord['message'] && typeof eventRecord['message'] === 'object' && !Array.isArray(eventRecord['message']))
|
||||
@@ -658,6 +717,7 @@ export class MessagesEffects {
|
||||
webrtc: this.webrtc,
|
||||
attachments: this.attachments,
|
||||
debugging: this.debugging,
|
||||
messageRevisions: this.messageRevisions,
|
||||
currentUser: currentUser ?? null,
|
||||
currentRoom,
|
||||
savedRooms
|
||||
|
||||
Reference in New Issue
Block a user