Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/chat/services/ChatAuditService.server.ts
T

291 lines
9.7 KiB
TypeScript

import { CHAT_PERMISSIONS } from "@hivegrid/chat";
import {
forceArchiveChatAuditChannel,
forceRemoveChatAuditMember,
forceSetChatAuditMemberRole,
getChatAuditAdministration,
getChatAuditAttachmentDownload,
getChatAuditChannelDetail,
listChatAuditChannels,
recordChatAuditSessionStarted,
type ChatAuditAttachmentDownload,
type ChatAuditAttachmentVariant,
} from "@hivegrid/chat/audit.server";
import {
buildChatAuditAdministrationVM,
buildChatAuditOverviewVM,
buildChatChannelDetailVM,
type ChatAuditAdministrationVM,
type ChatAuditOverviewVM,
type ChatChannelDetailVM,
type ChatResolvedAuthorVM,
} from "@hivegrid/view-models";
import { requireChatPermission, type ChatRouteContext } from "../routes/chatAccess.server";
import { chatAuditAttachmentPath, chatAuditPath } from "../routes/chatRoutePaths";
import { buildChatResolvedAuthors, buildMemberChatChannelDetailVM } from "./chatViewModel.server";
import type { ChatAuditSession } from "./chatAuditSession.server";
export type ChatAuditLabels = Readonly<{
directMessage: string;
unnamedGroup: string;
unnamedProject: string;
unknownUser: string;
unknownMention: string;
}>;
export async function loadChatAuditOverview(input: {
readonly context: ChatRouteContext;
readonly tenantSlug: string;
readonly session: ChatAuditSession | null;
readonly requestId?: string;
readonly labels: Pick<ChatAuditLabels, "directMessage" | "unnamedGroup" | "unnamedProject">;
}): Promise<ChatAuditOverviewVM> {
const context = requireChatPermission(input.context, CHAT_PERMISSIONS.readAllChannels);
const channels = input.session
? await listChatAuditChannels({
...repositoryContext(context),
requestId: input.requestId ?? crypto.randomUUID(),
auditSession: auditSessionContext(input.session),
})
: [];
return buildChatAuditOverviewVM({
canAdminister: context.permissions.includes(CHAT_PERMISSIONS.admin),
session: input.session
? {
id: input.session.id,
reason: input.session.reason,
expiresAtIso: new Date(input.session.expiresAt).toISOString(),
}
: null,
channels: channels.map((channel) => ({
id: channel.id,
kind: channel.kind,
title: resolveAuditTitle(channel, input.labels),
description: channel.description,
participantLabels: channel.participants.map((participant) => participant.displayName),
memberCount: channel.memberCount,
isPrivate: channel.isPrivate,
archivedAtIso: channel.archivedAtIso,
lastMessageAtIso: channel.lastMessageAtIso,
detailHref: `${chatAuditPath(input.tenantSlug)}/${encodeURIComponent(channel.id)}`,
})),
});
}
export async function startChatAuditSession(input: {
readonly context: ChatRouteContext;
readonly session: ChatAuditSession;
readonly requestId: string;
}): Promise<void> {
const context = requireChatPermission(input.context, CHAT_PERMISSIONS.readAllChannels);
await recordChatAuditSessionStarted({
...repositoryContext(context),
requestId: input.requestId,
auditSession: auditSessionContext(input.session),
});
}
export async function loadChatAuditChannel(input: {
readonly context: ChatRouteContext;
readonly tenantSlug: string;
readonly chatId: string;
readonly session: ChatAuditSession;
readonly requestId: string;
readonly beforeMessageCursor?: string;
readonly labels: ChatAuditLabels;
}): Promise<{
readonly vm: ChatChannelDetailVM;
readonly administrationVm: ChatAuditAdministrationVM;
readonly authors: readonly ChatResolvedAuthorVM[];
}> {
const context = requireChatPermission(input.context, CHAT_PERMISSIONS.readAllChannels);
const [detail, administration] = await Promise.all([
getChatAuditChannelDetail({
...repositoryContext(context),
chatId: input.chatId,
requestId: input.requestId,
auditSession: auditSessionContext(input.session),
...(input.beforeMessageCursor ? { beforeMessageCursor: input.beforeMessageCursor } : {}),
}),
getChatAuditAdministration({ ...repositoryContext(context), chatId: input.chatId }),
]);
const baseVm = buildMemberChatChannelDetailVM({
detail,
tenantSlug: input.tenantSlug,
currentUserId: context.userId,
permissions: [],
nowIso: new Date().toISOString(),
labels: input.labels,
});
const title = resolveAuditTitle(detail.channel, input.labels);
const administrationVm = buildChatAuditAdministrationVM({
channel: {
id: administration.channel.id,
kind: administration.channel.kind,
title,
archivedAtIso: administration.channel.archivedAtIso,
},
canAdminister: administration.canAdminister,
members: administration.members,
});
if (baseVm.status !== "ready") return { vm: baseVm, administrationVm, authors: [] };
const auditedMessages = baseVm.data.messages.map((message) => ({
...message,
attachments: message.attachments.map((attachment) => ({
...attachment,
downloadHref: chatAuditAttachmentPath(input.tenantSlug, input.chatId, attachment.fileId, "original"),
previewHref: attachment.previewHref ? chatAuditAttachmentPath(input.tenantSlug, input.chatId, attachment.fileId, "preview") : null,
})),
}));
const vm = buildChatChannelDetailVM({
header: {
...baseVm.data.header,
title,
avatar: { imageUrl: null, fallbackLabel: initials(title) },
isAuditMode: true,
canManage: false,
settingsHref: null,
searchHref: chatAuditPath(input.tenantSlug),
},
messages: auditedMessages,
pinnedMessages: baseVm.data.pinnedMessages,
typingLabels: [],
history: baseVm.data.history,
composer: {
...baseVm.data.composer,
canSend: false,
canAttachFiles: false,
canRecordVoice: false,
canMention: false,
canMarkAsTask: false,
disabledReasonKey: "chat.composer.auditReadOnly",
},
connection: { ...baseVm.data.connection, state: "blocked", reconnectAttempt: 0, retryAtIso: null },
});
return {
vm,
administrationVm,
authors: buildChatResolvedAuthors(detail.channel.participants, context.userId),
};
}
export async function loadChatAuditAttachment(input: {
readonly context: ChatRouteContext;
readonly chatId: string;
readonly fileId: string;
readonly variant: ChatAuditAttachmentVariant;
readonly session: ChatAuditSession;
readonly requestId: string;
}): Promise<ChatAuditAttachmentDownload> {
const context = requireChatPermission(input.context, CHAT_PERMISSIONS.readAllChannels);
return getChatAuditAttachmentDownload({
...repositoryContext(context),
chatId: input.chatId,
fileId: input.fileId,
variant: input.variant,
requestId: input.requestId,
auditSession: auditSessionContext(input.session),
});
}
export async function archiveChatAuditChannel(input: {
readonly context: ChatRouteContext;
readonly chatId: string;
readonly session: ChatAuditSession;
readonly requestId: string;
}): Promise<void> {
const context = requireChatAuditAdministrator(input.context);
await forceArchiveChatAuditChannel({
...repositoryContext(context),
chatId: input.chatId,
requestId: input.requestId,
auditSession: auditSessionContext(input.session),
});
}
export async function setChatAuditMemberRole(input: {
readonly context: ChatRouteContext;
readonly chatId: string;
readonly memberUserId: string;
readonly roleInChat: "admin" | "member";
readonly session: ChatAuditSession;
readonly requestId: string;
}): Promise<void> {
const context = requireChatAuditAdministrator(input.context);
await forceSetChatAuditMemberRole({
...repositoryContext(context),
chatId: input.chatId,
memberUserId: input.memberUserId,
roleInChat: input.roleInChat,
requestId: input.requestId,
auditSession: auditSessionContext(input.session),
});
}
export async function removeChatAuditMember(input: {
readonly context: ChatRouteContext;
readonly chatId: string;
readonly memberUserId: string;
readonly session: ChatAuditSession;
readonly requestId: string;
}): Promise<void> {
const context = requireChatAuditAdministrator(input.context);
await forceRemoveChatAuditMember({
...repositoryContext(context),
chatId: input.chatId,
memberUserId: input.memberUserId,
requestId: input.requestId,
auditSession: auditSessionContext(input.session),
});
}
function requireChatAuditAdministrator(context: ChatRouteContext): ChatRouteContext {
return requireChatPermission(requireChatPermission(context, CHAT_PERMISSIONS.readAllChannels), CHAT_PERMISSIONS.admin);
}
function repositoryContext(context: ChatRouteContext) {
return { tenantId: context.tenantId, userId: context.userId, permissions: context.permissions };
}
function auditSessionContext(session: ChatAuditSession) {
return {
sessionId: session.id,
reason: session.reason,
expiresAtIso: new Date(session.expiresAt).toISOString(),
};
}
function resolveAuditTitle(
channel: {
readonly kind: "project" | "group" | "dm";
readonly name: string | null;
readonly participants: readonly { readonly displayName: string }[];
},
labels: Pick<ChatAuditLabels, "directMessage" | "unnamedGroup" | "unnamedProject">
): string {
const name = channel.name?.trim();
if (name) return name;
if (channel.kind === "dm") {
return (
channel.participants
.map((participant) => participant.displayName.trim())
.filter(Boolean)
.join(" · ") || labels.directMessage
);
}
return channel.kind === "project" ? labels.unnamedProject : labels.unnamedGroup;
}
function initials(label: string): string {
return (
label
.trim()
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toLocaleUpperCase() ?? "")
.join("") || "?"
);
}