fix and update

This commit is contained in:
lendry
2026-07-01 19:19:27 +03:00
parent f423f512f8
commit 7e54cec361
16 changed files with 1093 additions and 150 deletions

View File

@@ -1,5 +1,9 @@
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../infra/prisma.service';
import {
AdminInsightsQueryOptions,
AdminInsightsRetentionService
} from './admin-insights-retention.service';
const MODERATABLE_ROOM_FILTER = {
isE2E: false,
@@ -23,7 +27,10 @@ const ROOM_TYPE_LABELS: Record<string, string> = {
@Injectable()
export class AdminUserInsightsService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly retention: AdminInsightsRetentionService
) {}
async ensureHumanUser(userId: string) {
const user = await this.prisma.user.findFirst({
@@ -35,14 +42,17 @@ export class AdminUserInsightsService {
return user;
}
async getSignInHistory(userId: string, search?: string, limit = 50, offset = 0) {
async getSignInHistory(userId: string, options: AdminInsightsQueryOptions = {}) {
await this.ensureHumanUser(userId);
const take = Math.min(Math.max(limit, 1), 100);
const skip = Math.max(offset, 0);
const searchTerm = search?.trim();
const range = await this.retention.resolveDateRange(options.dateFrom, options.dateTo);
const take = Math.min(Math.max(options.limit ?? 50, 1), 100);
const skip = Math.max(options.offset ?? 0, 0);
const searchTerm = options.search?.trim();
const createdAt = this.retention.createdAtFilter(range);
const where = {
userId,
createdAt,
...(searchTerm
? {
OR: [
@@ -67,6 +77,7 @@ export class AdminUserInsightsService {
]);
return {
...this.retention.insightsMeta(range),
total,
events: events.map((event) => ({
id: event.id,
@@ -80,19 +91,21 @@ export class AdminUserInsightsService {
};
}
async getActivityTimeline(userId: string, search?: string, limit = 50, offset = 0) {
async getActivityTimeline(userId: string, options: AdminInsightsQueryOptions = {}) {
await this.ensureHumanUser(userId);
const searchTerm = search?.trim().toLowerCase();
const take = Math.min(Math.max(limit, 1), 100);
const range = await this.retention.resolveDateRange(options.dateFrom, options.dateTo);
const searchTerm = options.search?.trim().toLowerCase();
const take = Math.min(Math.max(options.limit ?? 50, 1), 100);
const createdAt = this.retention.createdAtFilter(range);
const [logs, documents, messages, rooms, families, consents] = await Promise.all([
this.prisma.userActivityLog.findMany({
where: { userId },
where: { userId, createdAt },
orderBy: { createdAt: 'desc' },
take: 200
}),
this.prisma.userDocument.findMany({
where: { userId },
where: { userId, createdAt },
orderBy: { createdAt: 'desc' },
take: 100
}),
@@ -100,6 +113,7 @@ export class AdminUserInsightsService {
where: {
senderId: userId,
deletedAt: null,
createdAt,
room: MODERATABLE_ROOM_FILTER
},
include: { room: { include: { group: true } } },
@@ -107,18 +121,18 @@ export class AdminUserInsightsService {
take: 100
}),
this.prisma.chatRoom.findMany({
where: { createdById: userId, ...MODERATABLE_ROOM_FILTER },
where: { createdById: userId, createdAt, ...MODERATABLE_ROOM_FILTER },
include: { group: true },
orderBy: { createdAt: 'desc' },
take: 50
}),
this.prisma.familyGroup.findMany({
where: { ownerId: userId },
where: { ownerId: userId, createdAt },
orderBy: { createdAt: 'desc' },
take: 50
}),
this.prisma.oAuthConsent.findMany({
where: { userId },
where: { userId, createdAt },
include: { client: true },
orderBy: { createdAt: 'desc' },
take: 50
@@ -200,24 +214,33 @@ export class AdminUserInsightsService {
return haystack.includes(searchTerm);
});
const skip = Math.max(offset, 0);
const skip = Math.max(options.offset ?? 0, 0);
return {
...this.retention.insightsMeta(range),
total: deduped.length,
items: deduped.slice(skip, skip + take)
};
}
async listUserChatRooms(userId: string, search?: string, limit = 50, offset = 0) {
async listUserChatRooms(userId: string, options: AdminInsightsQueryOptions = {}) {
await this.ensureHumanUser(userId);
const take = Math.min(Math.max(limit, 1), 100);
const skip = Math.max(offset, 0);
const searchTerm = search?.trim();
const range = await this.retention.resolveDateRange(options.dateFrom, options.dateTo);
const take = Math.min(Math.max(options.limit ?? 50, 1), 100);
const skip = Math.max(options.offset ?? 0, 0);
const searchTerm = options.search?.trim();
const createdAt = this.retention.createdAtFilter(range);
const memberships = await this.prisma.chatRoomMember.findMany({
where: {
userId,
room: {
...MODERATABLE_ROOM_FILTER,
messages: {
some: {
senderId: userId,
createdAt
}
},
...(searchTerm
? {
OR: [
@@ -238,12 +261,18 @@ export class AdminUserInsightsService {
}
},
messages: {
where: { deletedAt: null },
where: { deletedAt: null, createdAt },
orderBy: { createdAt: 'desc' },
take: 1,
include: { sender: { select: { displayName: true } } }
},
_count: { select: { messages: true } }
_count: {
select: {
messages: {
where: { senderId: userId, createdAt }
}
}
}
}
}
},
@@ -278,13 +307,15 @@ export class AdminUserInsightsService {
});
return {
...this.retention.insightsMeta(range),
total: rooms.length,
rooms: rooms.slice(skip, skip + take)
};
}
async listRoomMessages(userId: string, roomId: string, search?: string, limit = 50, beforeMessageId?: string) {
async listRoomMessages(userId: string, roomId: string, options: AdminInsightsQueryOptions = {}) {
await this.ensureHumanUser(userId);
const range = await this.retention.resolveDateRange(options.dateFrom, options.dateTo);
const room = await this.getModeratableRoom(roomId);
const member = await this.prisma.chatRoomMember.findUnique({
where: { roomId_userId: { roomId, userId } }
@@ -293,19 +324,21 @@ export class AdminUserInsightsService {
throw new ForbiddenException('Пользователь не состоит в этом чате');
}
const take = Math.min(Math.max(limit, 1), 100);
const searchTerm = search?.trim();
const take = Math.min(Math.max(options.limit ?? 50, 1), 100);
const searchTerm = options.search?.trim();
const createdAt = this.retention.createdAtFilter(range);
let cursorDate: Date | undefined;
if (beforeMessageId) {
const cursor = await this.prisma.chatMessage.findFirst({ where: { id: beforeMessageId, roomId } });
if (options.beforeMessageId) {
const cursor = await this.prisma.chatMessage.findFirst({ where: { id: options.beforeMessageId, roomId } });
if (cursor) cursorDate = cursor.createdAt;
}
const messages = await this.prisma.chatMessage.findMany({
where: {
roomId,
...(cursorDate ? { createdAt: { lt: cursorDate } } : {}),
createdAt,
...(cursorDate ? { createdAt: { lt: cursorDate, gte: range.from, lte: range.to } } : {}),
...(searchTerm
? {
OR: [
@@ -321,25 +354,30 @@ export class AdminUserInsightsService {
});
return {
...this.retention.insightsMeta(range),
room: {
id: room.id,
name: room.name,
type: room.type,
groupName: room.group.name
},
messages: messages.reverse().map((message) => this.toAdminMessage(message))
messages: messages.reverse().map((message) =>
this.toAdminMessage(message, { roomName: room.name, groupName: room.group.name })
)
};
}
async searchUserChatMessages(userId: string, search?: string, limit = 50, offset = 0) {
async searchUserChatMessages(userId: string, options: AdminInsightsQueryOptions) {
await this.ensureHumanUser(userId);
const searchTerm = search?.trim();
const searchTerm = options.search?.trim();
if (!searchTerm) {
throw new BadRequestException('Укажите текст для поиска по сообщениям');
}
const take = Math.min(Math.max(limit, 1), 100);
const skip = Math.max(offset, 0);
const range = await this.retention.resolveDateRange(options.dateFrom, options.dateTo);
const take = Math.min(Math.max(options.limit ?? 50, 1), 100);
const skip = Math.max(options.offset ?? 0, 0);
const createdAt = this.retention.createdAtFilter(range);
const where = {
room: {
@@ -347,6 +385,7 @@ export class AdminUserInsightsService {
...MODERATABLE_ROOM_FILTER
},
deletedAt: null,
createdAt,
OR: [
{ content: { contains: searchTerm, mode: 'insensitive' as const } },
{ sender: { displayName: { contains: searchTerm, mode: 'insensitive' as const } } }
@@ -368,12 +407,13 @@ export class AdminUserInsightsService {
]);
return {
...this.retention.insightsMeta(range),
total,
messages: messages.map((message) => ({
...this.toAdminMessage(message),
roomId: message.roomId,
roomName: message.room.name,
groupName: message.room.group.name
...this.toAdminMessage(message, {
roomName: message.room.name,
groupName: message.room.group.name
})
}))
};
}
@@ -447,17 +487,24 @@ export class AdminUserInsightsService {
return text.length > 160 ? `${text.slice(0, 157)}` : text;
}
private toAdminMessage(message: {
id: string;
senderId: string;
type: string;
content: string | null;
isEncrypted: boolean;
deletedAt: Date | null;
createdAt: Date;
editedAt: Date | null;
sender: { id: string; displayName: string };
}) {
private toAdminMessage(
message: {
id: string;
roomId: string;
senderId: string;
type: string;
content: string | null;
storageKey: string | null;
mimeType: string | null;
metadata: unknown;
isEncrypted: boolean;
deletedAt: Date | null;
createdAt: Date;
editedAt: Date | null;
sender: { id: string; displayName: string };
},
context?: { roomName?: string; groupName?: string }
) {
return {
id: message.id,
senderId: message.senderId,
@@ -468,7 +515,18 @@ export class AdminUserInsightsService {
isEncrypted: message.isEncrypted,
isDeleted: Boolean(message.deletedAt),
createdAt: message.createdAt.toISOString(),
editedAt: message.editedAt?.toISOString()
editedAt: message.editedAt?.toISOString(),
roomId: message.roomId,
roomName: context?.roomName,
groupName: context?.groupName,
storageKey: message.deletedAt || message.isEncrypted ? undefined : message.storageKey ?? undefined,
mimeType: message.deletedAt || message.isEncrypted ? undefined : message.mimeType ?? undefined,
metadataJson:
message.deletedAt || message.isEncrypted
? undefined
: message.metadata
? JSON.stringify(message.metadata)
: undefined
};
}
}