From d1c5ac7e1950012a96a5d04d11048d4361567353 Mon Sep 17 00:00:00 2001
From: Sergey Krylov
Date: Mon, 28 Jul 2025 05:48:36 +0300
Subject: [PATCH] add cron operation
---
backend/src/module/cron/cron.module.ts | 3 +-
backend/src/module/cron/cron.service.ts | 70 +++++++++++++++++++
backend/src/module/libs/mail/mail.service.ts | 23 ++++--
.../templates/enable-two-factor.template.tsx | 68 ++++++++++++++++++
.../src/module/libs/mail/templates/index.ts | 6 ++
.../templates/verify-channel.template.tsx | 56 +++++++++++++++
.../module/libs/telegram/telegram.message.ts | 1 +
.../module/libs/telegram/telegram.service.ts | 8 +++
8 files changed, 230 insertions(+), 5 deletions(-)
create mode 100644 backend/src/module/libs/mail/templates/enable-two-factor.template.tsx
create mode 100644 backend/src/module/libs/mail/templates/index.ts
create mode 100644 backend/src/module/libs/mail/templates/verify-channel.template.tsx
diff --git a/backend/src/module/cron/cron.module.ts b/backend/src/module/cron/cron.module.ts
index 42f541c..2642f95 100644
--- a/backend/src/module/cron/cron.module.ts
+++ b/backend/src/module/cron/cron.module.ts
@@ -1,9 +1,10 @@
+import { NotificationService } from '@/src/module/notification/notification.service'
import { Module } from '@nestjs/common'
import { ScheduleModule } from '@nestjs/schedule'
import { CronService } from './cron.service'
@Module({
imports: [ScheduleModule.forRoot()],
- providers: [CronService],
+ providers: [CronService, NotificationService],
})
export class CronModule {}
diff --git a/backend/src/module/cron/cron.service.ts b/backend/src/module/cron/cron.service.ts
index 7bd548e..c6d2afc 100644
--- a/backend/src/module/cron/cron.service.ts
+++ b/backend/src/module/cron/cron.service.ts
@@ -2,6 +2,7 @@ import { PrismaService } from '@/src/core/prisma/prisma.service'
import { MailService } from '@/src/module/libs/mail/mail.service'
import { StorageService } from '@/src/module/libs/storage/storage.service'
import { TelegramService } from '@/src/module/libs/telegram/telegram.service'
+import { NotificationService } from '@/src/module/notification/notification.service'
import { Injectable } from '@nestjs/common'
import { Cron, CronExpression } from '@nestjs/schedule'
@@ -12,6 +13,7 @@ export class CronService {
private readonly mailService: MailService,
private readonly storageService: StorageService,
private readonly telegramService: TelegramService,
+ private readonly notificationService: NotificationService,
) {
}
@@ -53,4 +55,72 @@ export class CronService {
},
})
}
+
+ @Cron('0 0 */4 * *')
+ public async notifyUsersEnableTwoFactor() {
+ const users = await this.prismaService.user.findMany({
+ where: { isTotpEnabled: false },
+ include: { notificationSettings: true },
+ })
+
+ for (const user of users) {
+ if (!user) {
+ continue
+ }
+
+ await this.mailService.sendEnableTwoFactor(user.email)
+
+ if (user.notificationSettings?.siteNotifications) {
+ await this.notificationService.createEnableTwoFactor(user.id)
+ }
+
+ if (user.notificationSettings?.telegramNotifications && user.telegramId) {
+ await this.telegramService.sendEnableTwoFactor(user.telegramId)
+ }
+ }
+ }
+
+ @Cron(CronExpression.EVERY_DAY_AT_1AM)
+ public async verifyChannels() {
+ const users = await this.prismaService.user.findMany({
+ include: { notificationSettings: true },
+ })
+
+ for (const user of users) {
+ const followersCount = await this.prismaService.follow.count({
+ where: { followingId: user.id },
+ })
+
+ if (followersCount > 10 && !user.isVerified) {
+ await this.prismaService.user.update({
+ where: { id: user.id },
+ data: { isVerified: true },
+ })
+
+ await this.mailService.sendVerifyChannel(user.email)
+
+ if (user.notificationSettings?.siteNotifications) {
+ await this.notificationService.createVerifyChannel(user.id)
+ }
+
+ if (user.notificationSettings?.telegramNotifications && user.telegramId) {
+ await this.telegramService.sendVerifyChannel(user.telegramId)
+ }
+ }
+ }
+ }
+
+ @Cron(CronExpression.EVERY_DAY_AT_1AM)
+ public async deleteOldNotifications() {
+ const sevenDaysAgo = new Date()
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)
+
+ await this.prismaService.notification.deleteMany({
+ where: {
+ createdAt: {
+ lte: sevenDaysAgo,
+ },
+ },
+ })
+ }
}
diff --git a/backend/src/module/libs/mail/mail.service.ts b/backend/src/module/libs/mail/mail.service.ts
index 1f47a79..18bd40d 100644
--- a/backend/src/module/libs/mail/mail.service.ts
+++ b/backend/src/module/libs/mail/mail.service.ts
@@ -1,14 +1,16 @@
-import { AccountDeletionTemplate } from '@/src/module/libs/mail/templates/account-deletion.template'
-import { DeactivateTemplate } from '@/src/module/libs/mail/templates/deactivate.template'
-import PasswordRecoveryTemplate from '@/src/module/libs/mail/templates/password-recovery.template'
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
import { Token } from '@prisma/generated'
import { ProcessEnv } from '@/src/shared/types/env'
-import VerificationTemplate from './templates/verification.template'
import { MailerService } from '@nestjs-modules/mailer'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { render } from '@react-email/components'
+import { EnableTwoFactorTemplate } from './templates'
+import { VerifyChannelTemplate } from './templates'
+import { VerificationTemplate } from './templates'
+import { PasswordRecoveryTemplate } from './templates'
+import { DeactivateTemplate } from './templates'
+import { AccountDeletionTemplate } from './templates'
@Injectable()
export class MailService {
@@ -44,6 +46,19 @@ export class MailService {
void this.sendMail(email, 'Удаление аккаунта', html)
}
+ public async sendEnableTwoFactor(email: string) {
+ const domain = this.configService.getOrThrow('ALLOWED_ORIGIN')
+ const html = await render(EnableTwoFactorTemplate({ domain }))
+
+ void this.sendMail(email, 'Обеспечьте свою безопасность', html)
+ }
+
+ public async sendVerifyChannel(email: string) {
+ const html = await render(VerifyChannelTemplate())
+
+ void this.sendMail(email, 'Ваш канал верифицирован', html)
+ }
+
private sendMail(email: string, subject: string, html: string) {
return this.mailerService.sendMail({
to: email,
diff --git a/backend/src/module/libs/mail/templates/enable-two-factor.template.tsx b/backend/src/module/libs/mail/templates/enable-two-factor.template.tsx
new file mode 100644
index 0000000..1433240
--- /dev/null
+++ b/backend/src/module/libs/mail/templates/enable-two-factor.template.tsx
@@ -0,0 +1,68 @@
+import {
+ Body,
+ Head,
+ Heading,
+ Html,
+ Link,
+ Preview,
+ Section,
+ Tailwind,
+ Text,
+} from '@react-email/components'
+import * as React from 'react'
+
+interface EnableTwoFactorTemplateProps {
+ domain: string
+}
+
+export function EnableTwoFactorTemplate({ domain }: EnableTwoFactorTemplateProps) {
+ const settingsLink = `${domain}/dashboard/settings`
+
+ return (
+
+
+ Обеспечьте свою безопасность
+
+
+
+
+ Защитите свой аккаунт с двухфакторной аутентификацией
+
+
+ Включите двухфакторную аутентификацию, чтобы повысить безопасность вашего аккаунта.
+
+
+
+
+
+ Почему это важно?
+
+
+ Двухфакторная аутентификация добавляет дополнительный уровень защиты, требуя код, который известен только вам.
+
+
+ Перейти в настройки аккаунта
+
+
+
+
+
+ Если у вас возникли вопросы, обращайтесь в службу поддержки по адресу
+ {' '}
+
+ help@teastream.ru
+
+ .
+
+
+
+
+
+ )
+}
diff --git a/backend/src/module/libs/mail/templates/index.ts b/backend/src/module/libs/mail/templates/index.ts
new file mode 100644
index 0000000..58a81f5
--- /dev/null
+++ b/backend/src/module/libs/mail/templates/index.ts
@@ -0,0 +1,6 @@
+export { default as VerificationTemplate } from './verification.template'
+export { AccountDeletionTemplate } from './account-deletion.template'
+export { DeactivateTemplate } from './deactivate.template'
+export { EnableTwoFactorTemplate } from './enable-two-factor.template'
+export { default as PasswordRecoveryTemplate } from './password-recovery.template'
+export { VerifyChannelTemplate } from './verify-channel.template'
diff --git a/backend/src/module/libs/mail/templates/verify-channel.template.tsx b/backend/src/module/libs/mail/templates/verify-channel.template.tsx
new file mode 100644
index 0000000..27b4311
--- /dev/null
+++ b/backend/src/module/libs/mail/templates/verify-channel.template.tsx
@@ -0,0 +1,56 @@
+import {
+ Body,
+ Head,
+ Heading,
+ Html,
+ Link,
+ Preview,
+ Section,
+ Tailwind,
+ Text,
+} from '@react-email/components'
+import * as React from 'react'
+
+export function VerifyChannelTemplate() {
+ return (
+
+
+ Ваш канал верифицирован
+
+
+
+
+ Поздравляем! Ваш канал верифицирован
+
+
+ Мы рады сообщить, что ваш канал теперь верифицирован, и вы получили официальный значок.
+
+
+
+
+
+ Что это значит?
+
+
+ Значок верификации подтверждает подлинность вашего канала и улучшает доверие зрителей.
+
+
+
+
+
+ Если у вас есть вопросы, напишите нам на
+ {' '}
+
+ help@teastream.ru
+
+ .
+
+
+
+
+
+ )
+}
diff --git a/backend/src/module/libs/telegram/telegram.message.ts b/backend/src/module/libs/telegram/telegram.message.ts
index 1d290d9..082267d 100644
--- a/backend/src/module/libs/telegram/telegram.message.ts
+++ b/backend/src/module/libs/telegram/telegram.message.ts
@@ -62,6 +62,7 @@ export const MESSAGES = {
`📡 На канале ${channel.displayName} началась трансляция!\n\n`
+ `Смотрите здесь: Перейти к трансляции`,
newFollowing: (follower: User, followersCount: number) =>
+
`У вас новый подписчик!\n\nЭто пользователь ${follower.displayName}\n\nИтоговое количество подписчиков на вашем канале: ${followersCount}`,
enableTwoFactor:
`🔐 Обеспечьте свою безопасность!\n\n`
diff --git a/backend/src/module/libs/telegram/telegram.service.ts b/backend/src/module/libs/telegram/telegram.service.ts
index 4bc311e..f9f775d 100644
--- a/backend/src/module/libs/telegram/telegram.service.ts
+++ b/backend/src/module/libs/telegram/telegram.service.ts
@@ -144,6 +144,14 @@ export class TelegramService extends Telegraf {
await this.telegram.sendMessage(chatId, MESSAGES.newFollowing(follower, user.followings.length), { parse_mode: 'HTML' })
}
+ public async sendEnableTwoFactor(chatId: string) {
+ return this.telegram.sendMessage(chatId, MESSAGES.enableTwoFactor, { parse_mode: 'HTML' })
+ }
+
+ public async sendVerifyChannel(chatId: string) {
+ return this.telegram.sendMessage(chatId, MESSAGES.verifyChannel, { parse_mode: 'HTML' })
+ }
+
private async connectTelegram(userId: string, chatId: string) {
return this.prismaService.user.update({
where: { id: userId },