add push settings

This commit is contained in:
lendry
2026-07-07 13:58:01 +03:00
parent 1bd95fa99e
commit 57925fb2c4
25 changed files with 3036 additions and 1756 deletions

View File

@@ -22,10 +22,12 @@ import { AppReleaseService } from './app-release.service';
import { FamilyService } from './family.service';
import { MediaService } from './media.service';
import { NotificationsService } from './notifications.service';
import { PushDeviceService } from './push-device.service';
import { ChatService } from './chat.service';
import { PresenceService } from './presence.service';
import { TotpService } from './totp.service';
import { MessagingService } from '../infra/messaging.service';
import { FirebasePushService } from '../infra/firebase-push.service';
import { BotFatherService } from './bot/bot-father.service';
import { BotApiService } from './bot/bot-api.service';
import { BotInboundService } from './bot/bot-inbound.service';
@@ -53,9 +55,11 @@ export class AuthGrpcController {
private readonly family: FamilyService,
private readonly media: MediaService,
private readonly notifications: NotificationsService,
private readonly pushDevices: PushDeviceService,
private readonly chat: ChatService,
private readonly presence: PresenceService,
private readonly messaging: MessagingService,
private readonly firebasePush: FirebasePushService,
private readonly totp: TotpService,
private readonly botFather: BotFatherService,
private readonly botApi: BotApiService,
@@ -651,6 +655,14 @@ export class AuthGrpcController {
return this.messaging.sendTestDelivery(channel, command.target.trim());
}
@GrpcMethod('SettingsService', 'TestFirebasePush')
async testFirebasePush(command: { userId: string }) {
if (!command.userId?.trim()) {
throw new BadRequestException('Не указан пользователь для тестовой отправки');
}
return this.firebasePush.sendTestToUser(command.userId.trim());
}
@GrpcMethod('ProfileService', 'GetProfile')
getProfile(command: { userId: string }) {
return this.profile.getProfile(command.userId);
@@ -1092,6 +1104,16 @@ export class AuthGrpcController {
return this.notifications.deleteAll(command.userId);
}
@GrpcMethod('NotificationsService', 'RegisterPushToken')
registerPushToken(command: { userId: string; token: string; platform: string; deviceLabel?: string }) {
return this.pushDevices.register(command.userId, command.token, command.platform, command.deviceLabel);
}
@GrpcMethod('NotificationsService', 'UnregisterPushToken')
unregisterPushToken(command: { userId: string; token: string }) {
return this.pushDevices.unregister(command.userId, command.token);
}
@GrpcMethod('ChatService', 'ListRooms')
listChatRooms(command: { userId: string; groupId: string }) {
return this.chat.listRooms(command.userId, command.groupId);

View File

@@ -1,12 +1,14 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../infra/prisma.service';
import { NotificationPublisherService } from '../infra/notification-publisher.service';
import { FirebasePushService } from '../infra/firebase-push.service';
@Injectable()
export class NotificationsService {
constructor(
private readonly prisma: PrismaService,
private readonly publisher: NotificationPublisherService
private readonly publisher: NotificationPublisherService,
private readonly firebasePush: FirebasePushService
) {}
async create(
@@ -38,6 +40,19 @@ export class NotificationsService {
...(payload ?? {})
}
});
void this.firebasePush
.sendToUser({
userId,
type,
title,
message,
payload: {
notificationId: notification.id,
...(payload ?? {})
}
})
.catch(() => undefined);
}
return this.toResponse(notification);

View File

@@ -0,0 +1,75 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { PushPlatform } from '../generated/prisma/client';
import { PrismaService } from '../infra/prisma.service';
const VALID_PLATFORMS = new Set<string>(['WEB', 'ANDROID', 'IOS']);
@Injectable()
export class PushDeviceService {
constructor(private readonly prisma: PrismaService) {}
async register(userId: string, token: string, platform: string, deviceLabel?: string) {
const normalizedToken = token?.trim();
if (!normalizedToken) {
throw new BadRequestException('Укажите FCM-токен устройства');
}
const normalizedPlatform = (platform?.trim().toUpperCase() || 'WEB') as PushPlatform;
if (!VALID_PLATFORMS.has(normalizedPlatform)) {
throw new BadRequestException('Недопустимая платформа push-уведомлений');
}
const existingOwner = await this.prisma.pushDeviceToken.findUnique({
where: { token: normalizedToken }
});
if (existingOwner && existingOwner.userId !== userId) {
await this.prisma.pushDeviceToken.delete({ where: { token: normalizedToken } });
}
await this.prisma.pushDeviceToken.upsert({
where: { token: normalizedToken },
create: {
userId,
token: normalizedToken,
platform: normalizedPlatform,
deviceLabel: deviceLabel?.trim() || null
},
update: {
userId,
platform: normalizedPlatform,
deviceLabel: deviceLabel?.trim() || null
}
});
return { count: 1 };
}
async unregister(userId: string, token: string) {
const normalizedToken = token?.trim();
if (!normalizedToken) {
throw new BadRequestException('Укажите FCM-токен устройства');
}
const result = await this.prisma.pushDeviceToken.deleteMany({
where: { userId, token: normalizedToken }
});
return { count: result.count };
}
async listTokensForUser(userId: string) {
return this.prisma.pushDeviceToken.findMany({
where: { userId },
orderBy: { updatedAt: 'desc' }
});
}
async removeTokens(tokens: string[]) {
if (!tokens.length) return { count: 0 };
const result = await this.prisma.pushDeviceToken.deleteMany({
where: { token: { in: tokens } }
});
return { count: result.count };
}
}

View File

@@ -111,6 +111,30 @@ export const DEFAULT_SYSTEM_SETTINGS = [
key: 'ADMIN_INSIGHTS_RETENTION_DAYS',
value: '7',
description: 'Срок хранения журналов входов, активности и переписок для админки (дни, 190). Старые записи удаляются автоматически'
},
{ key: 'FIREBASE_PUSH_ENABLED', value: 'false', description: 'Включить push-уведомления через Firebase Cloud Messaging (FCM)' },
{ key: 'FIREBASE_PROJECT_ID', value: '', description: 'ID проекта Firebase (project_id из консоли Firebase)' },
{
key: 'FIREBASE_CLIENT_EMAIL',
value: '',
description: 'Email сервисного аккаунта Firebase (client_email из JSON ключа)'
},
{
key: 'FIREBASE_PRIVATE_KEY',
value: '',
description: 'Приватный ключ сервисного аккаунта Firebase (private_key из JSON, с символами \\n)'
},
{ key: 'FIREBASE_WEB_API_KEY', value: '', description: 'Web API Key из настроек Firebase (публичный, для клиентского SDK)' },
{ key: 'FIREBASE_WEB_APP_ID', value: '', description: 'App ID веб-приложения Firebase (публичный)' },
{
key: 'FIREBASE_WEB_MESSAGING_SENDER_ID',
value: '',
description: 'Messaging Sender ID Firebase (публичный, для клиентского SDK)'
},
{
key: 'FIREBASE_WEB_VAPID_KEY',
value: '',
description: 'VAPID / Web Push certificate key pair из Firebase Cloud Messaging (публичный ключ)'
}
] as const;
@@ -119,7 +143,8 @@ const SECRET_SETTING_KEYS = new Set([
'EMAIL_SMTP_PASSWORD',
'SMS_API_KEY',
'SMS_PASSWORD',
'SMS_GATEWAY_PASSWORD'
'SMS_GATEWAY_PASSWORD',
'FIREBASE_PRIVATE_KEY'
]);
export const PUBLIC_SETTING_KEYS = [
@@ -133,7 +158,13 @@ export const PUBLIC_SETTING_KEYS = [
'MAINTENANCE_MODE',
'LDAP_ENABLED',
'LDAP_USE_LDAPS',
'PIN_LOCK_TIMEOUT_MINUTES'
'PIN_LOCK_TIMEOUT_MINUTES',
'FIREBASE_PUSH_ENABLED',
'FIREBASE_PROJECT_ID',
'FIREBASE_WEB_API_KEY',
'FIREBASE_WEB_APP_ID',
'FIREBASE_WEB_MESSAGING_SENDER_ID',
'FIREBASE_WEB_VAPID_KEY'
] as const;
@Injectable()