fix and update
This commit is contained in:
@@ -5,6 +5,7 @@ import { AccessService } from './domain/access.service';
|
||||
import { AdminSeedService } from './domain/admin-seed.service';
|
||||
import { AdminService } from './domain/admin.service';
|
||||
import { AdminUserInsightsService } from './domain/admin-user-insights.service';
|
||||
import { AdminInsightsRetentionService } from './domain/admin-insights-retention.service';
|
||||
import { ActivityLogService } from './domain/activity-log.service';
|
||||
import { AuthGrpcController } from './domain/auth-grpc.controller';
|
||||
import { AuthService } from './domain/auth.service';
|
||||
@@ -73,6 +74,7 @@ import { BotFatherAssistantService } from './domain/bot/bot-father-assistant.ser
|
||||
AdminSeedService,
|
||||
AdminService,
|
||||
AdminUserInsightsService,
|
||||
AdminInsightsRetentionService,
|
||||
ActivityLogService,
|
||||
RbacService,
|
||||
OAuthSslSansService,
|
||||
|
||||
138
apps/sso-core/src/domain/admin-insights-retention.service.ts
Normal file
138
apps/sso-core/src/domain/admin-insights-retention.service.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../infra/prisma.service';
|
||||
import { MinioService } from '../infra/minio.service';
|
||||
import { SettingsService } from './settings.service';
|
||||
|
||||
export const ADMIN_INSIGHTS_RETENTION_DAYS_KEY = 'ADMIN_INSIGHTS_RETENTION_DAYS';
|
||||
export const DEFAULT_ADMIN_INSIGHTS_RETENTION_DAYS = 7;
|
||||
|
||||
const MODERATABLE_ROOM_FILTER = {
|
||||
isE2E: false,
|
||||
type: { notIn: ['E2E', 'BOT'] as string[] }
|
||||
};
|
||||
|
||||
export interface AdminInsightsDateRange {
|
||||
from: Date;
|
||||
to: Date;
|
||||
retentionDays: number;
|
||||
}
|
||||
|
||||
export interface AdminInsightsQueryOptions {
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
beforeMessageId?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminInsightsRetentionService {
|
||||
private readonly logger = new Logger(AdminInsightsRetentionService.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly settings: SettingsService,
|
||||
private readonly minio: MinioService
|
||||
) {}
|
||||
|
||||
async getRetentionDays(): Promise<number> {
|
||||
const days = await this.settings.getNumber(ADMIN_INSIGHTS_RETENTION_DAYS_KEY, DEFAULT_ADMIN_INSIGHTS_RETENTION_DAYS);
|
||||
return Math.min(Math.max(Math.trunc(days), 1), 90);
|
||||
}
|
||||
|
||||
getRetentionCutoff(retentionDays: number): Date {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - retentionDays);
|
||||
cutoff.setMilliseconds(0);
|
||||
return cutoff;
|
||||
}
|
||||
|
||||
private parseDateInput(value: string | undefined, label: string): Date | undefined {
|
||||
if (!value?.trim()) return undefined;
|
||||
const parsed = new Date(value.trim());
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
throw new BadRequestException(`Некорректная дата ${label}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async resolveDateRange(dateFrom?: string, dateTo?: string): Promise<AdminInsightsDateRange> {
|
||||
const retentionDays = await this.getRetentionDays();
|
||||
const cutoff = this.getRetentionCutoff(retentionDays);
|
||||
const now = new Date();
|
||||
|
||||
let from = this.parseDateInput(dateFrom, 'dateFrom') ?? cutoff;
|
||||
let to = this.parseDateInput(dateTo, 'dateTo') ?? now;
|
||||
|
||||
if (from < cutoff) from = cutoff;
|
||||
if (to > now) to = now;
|
||||
if (from > to) {
|
||||
throw new BadRequestException('Дата начала не может быть позже даты окончания');
|
||||
}
|
||||
|
||||
return { from, to, retentionDays };
|
||||
}
|
||||
|
||||
createdAtFilter(range: AdminInsightsDateRange) {
|
||||
return { gte: range.from, lte: range.to };
|
||||
}
|
||||
|
||||
insightsMeta(range: AdminInsightsDateRange) {
|
||||
return {
|
||||
retentionDays: range.retentionDays,
|
||||
periodFrom: range.from.toISOString(),
|
||||
periodTo: range.to.toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
async purgeExpiredInsights(): Promise<{ signInEvents: number; activityLogs: number; chatMessages: number }> {
|
||||
const retentionDays = await this.getRetentionDays();
|
||||
const cutoff = this.getRetentionCutoff(retentionDays);
|
||||
|
||||
const signInResult = await this.prisma.signInEvent.deleteMany({
|
||||
where: { createdAt: { lt: cutoff } }
|
||||
});
|
||||
|
||||
const activityResult = await this.prisma.userActivityLog.deleteMany({
|
||||
where: { createdAt: { lt: cutoff } }
|
||||
});
|
||||
|
||||
let chatMessages = 0;
|
||||
const batchSize = 500;
|
||||
while (true) {
|
||||
const staleMessages = await this.prisma.chatMessage.findMany({
|
||||
where: {
|
||||
createdAt: { lt: cutoff },
|
||||
room: MODERATABLE_ROOM_FILTER
|
||||
},
|
||||
select: { id: true, storageKey: true },
|
||||
take: batchSize
|
||||
});
|
||||
if (!staleMessages.length) break;
|
||||
|
||||
const storageKeys = staleMessages.map((item) => item.storageKey).filter((key): key is string => Boolean(key));
|
||||
if (storageKeys.length) {
|
||||
await this.minio.deleteObjects(storageKeys);
|
||||
}
|
||||
|
||||
const deleted = await this.prisma.chatMessage.deleteMany({
|
||||
where: { id: { in: staleMessages.map((item) => item.id) } }
|
||||
});
|
||||
chatMessages += deleted.count;
|
||||
if (staleMessages.length < batchSize) break;
|
||||
}
|
||||
|
||||
if (signInResult.count || activityResult.count || chatMessages) {
|
||||
this.logger.log(
|
||||
`Очистка журналов (${retentionDays} дн.): входы=${signInResult.count}, активность=${activityResult.count}, чаты=${chatMessages}`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
signInEvents: signInResult.count,
|
||||
activityLogs: activityResult.count,
|
||||
chatMessages
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,34 +187,64 @@ export class AuthGrpcController {
|
||||
}
|
||||
|
||||
@GrpcMethod('AdminService', 'GetUserSignInHistory')
|
||||
getUserSignInHistory(command: { userId: string; search?: string; limit?: number; offset?: number }) {
|
||||
return this.adminInsights.getSignInHistory(command.userId, command.search, command.limit, command.offset);
|
||||
getUserSignInHistory(command: {
|
||||
userId: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}) {
|
||||
return this.adminInsights.getSignInHistory(command.userId, command);
|
||||
}
|
||||
|
||||
@GrpcMethod('AdminService', 'GetUserActivity')
|
||||
getUserActivity(command: { userId: string; search?: string; limit?: number; offset?: number }) {
|
||||
return this.adminInsights.getActivityTimeline(command.userId, command.search, command.limit, command.offset);
|
||||
getUserActivity(command: {
|
||||
userId: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}) {
|
||||
return this.adminInsights.getActivityTimeline(command.userId, command);
|
||||
}
|
||||
|
||||
@GrpcMethod('AdminService', 'ListUserChatRooms')
|
||||
listUserChatRooms(command: { userId: string; search?: string; limit?: number; offset?: number }) {
|
||||
return this.adminInsights.listUserChatRooms(command.userId, command.search, command.limit, command.offset);
|
||||
listUserChatRooms(command: {
|
||||
userId: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}) {
|
||||
return this.adminInsights.listUserChatRooms(command.userId, command);
|
||||
}
|
||||
|
||||
@GrpcMethod('AdminService', 'ListUserChatMessages')
|
||||
listUserChatMessages(command: { userId: string; roomId: string; search?: string; limit?: number; beforeMessageId?: string }) {
|
||||
return this.adminInsights.listRoomMessages(
|
||||
command.userId,
|
||||
command.roomId,
|
||||
command.search,
|
||||
command.limit,
|
||||
command.beforeMessageId
|
||||
);
|
||||
listUserChatMessages(command: {
|
||||
userId: string;
|
||||
roomId: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
beforeMessageId?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}) {
|
||||
return this.adminInsights.listRoomMessages(command.userId, command.roomId, command);
|
||||
}
|
||||
|
||||
@GrpcMethod('AdminService', 'SearchUserChatMessages')
|
||||
searchUserChatMessages(command: { userId: string; search: string; limit?: number; offset?: number }) {
|
||||
return this.adminInsights.searchUserChatMessages(command.userId, command.search, command.limit, command.offset);
|
||||
searchUserChatMessages(command: {
|
||||
userId: string;
|
||||
search: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}) {
|
||||
return this.adminInsights.searchUserChatMessages(command.userId, command);
|
||||
}
|
||||
|
||||
@GrpcMethod('AdminService', 'AdminDeleteChatMessage')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { AdminInsightsRetentionService } from './admin-insights-retention.service';
|
||||
import { PinService } from './pin.service';
|
||||
import { ProfileService } from './profile.service';
|
||||
|
||||
@@ -11,7 +12,8 @@ export class MaintenanceSchedulerService implements OnModuleInit, OnModuleDestro
|
||||
|
||||
constructor(
|
||||
private readonly pin: PinService,
|
||||
private readonly profile: ProfileService
|
||||
private readonly profile: ProfileService,
|
||||
private readonly adminInsightsRetention: AdminInsightsRetentionService
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
@@ -49,5 +51,13 @@ export class MaintenanceSchedulerService implements OnModuleInit, OnModuleDestro
|
||||
`Ошибка финализации удаления аккаунтов: ${error instanceof Error ? error.message : error}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.adminInsightsRetention.purgeExpiredInsights();
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Ошибка очистки журналов админки: ${error instanceof Error ? error.message : error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ export class MediaService {
|
||||
}
|
||||
|
||||
async getChatMediaAccessUrl(requesterId: string, roomId: string, storageKey: string, fileName?: string) {
|
||||
await this.assertChatMember(roomId, requesterId);
|
||||
await this.assertChatMemberOrModerator(roomId, requesterId);
|
||||
if (!storageKey.startsWith(`chat/${roomId}/`)) {
|
||||
throw new BadRequestException('Некорректный ключ медиафайла');
|
||||
}
|
||||
@@ -379,6 +379,27 @@ export class MediaService {
|
||||
}
|
||||
}
|
||||
|
||||
private async assertChatMemberOrModerator(roomId: string, userId: string) {
|
||||
const member = await this.prisma.chatRoomMember.findFirst({ where: { roomId, userId } });
|
||||
if (member) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
||||
if (!user) {
|
||||
throw new ForbiddenException('Нет доступа к чату');
|
||||
}
|
||||
const access = await this.access.getUserAccess(userId, user.isSuperAdmin);
|
||||
if (!access.canModerateChats) {
|
||||
throw new ForbiddenException('Нет доступа к чату');
|
||||
}
|
||||
|
||||
const room = await this.prisma.chatRoom.findUnique({ where: { id: roomId } });
|
||||
if (!room || room.isE2E || room.type === 'E2E' || room.type === 'BOT') {
|
||||
throw new ForbiddenException('Этот чат недоступен для модерации');
|
||||
}
|
||||
}
|
||||
|
||||
private async publishUserAvatarUpdated(userId: string) {
|
||||
const memberships = await this.prisma.familyMember.findMany({
|
||||
where: { userId },
|
||||
|
||||
@@ -106,7 +106,12 @@ export const DEFAULT_SYSTEM_SETTINGS = [
|
||||
{ key: 'BOT_MAX_BOTS_PER_USER', value: '5', description: 'Максимальное количество ботов на одного пользователя' },
|
||||
{ key: 'BOT_API_RATE_LIMIT_PER_SECOND', value: '30', description: 'Лимит запросов Bot API на один токен в секунду' },
|
||||
{ key: 'BOT_WEBHOOK_TIMEOUT_MS', value: '10000', description: 'Таймаут HTTP-запроса webhook бота (мс)' },
|
||||
{ key: 'BOT_WEBHOOK_MAX_RETRIES', value: '3', description: 'Число повторов webhook с exponential backoff' }
|
||||
{ key: 'BOT_WEBHOOK_MAX_RETRIES', value: '3', description: 'Число повторов webhook с exponential backoff' },
|
||||
{
|
||||
key: 'ADMIN_INSIGHTS_RETENTION_DAYS',
|
||||
value: '7',
|
||||
description: 'Срок хранения журналов входов, активности и переписок для админки (дни, 1–90). Старые записи удаляются автоматически'
|
||||
}
|
||||
] as const;
|
||||
|
||||
const SECRET_SETTING_KEYS = new Set([
|
||||
|
||||
Reference in New Issue
Block a user