add cron operation

This commit is contained in:
Sergey Krylov 2025-07-28 05:48:36 +03:00
parent bfe3d9573c
commit d1c5ac7e19
8 changed files with 230 additions and 5 deletions

View File

@ -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 {}

View File

@ -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,
},
},
})
}
}

View File

@ -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<string>('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,

View File

@ -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 (
<Html>
<Head />
<Preview>Обеспечьте свою безопасность</Preview>
<Tailwind>
<Body className="max-w-2xl mx-auto p-6 bg-slate-50">
<Section className="text-center mb-8">
<Heading className="text-3xl text-black font-bold">
Защитите свой аккаунт с двухфакторной аутентификацией
</Heading>
<Text className="text-black text-base mt-2">
Включите двухфакторную аутентификацию, чтобы повысить безопасность вашего аккаунта.
</Text>
</Section>
<Section className="bg-white rounded-lg shadow-md p-6 text-center mb-6">
<Heading className="text-2xl text-black font-semibold">
Почему это важно?
</Heading>
<Text className="text-base text-black mt-2">
Двухфакторная аутентификация добавляет дополнительный уровень защиты, требуя код, который известен только вам.
</Text>
<Link
href={settingsLink}
className="inline-flex justify-center items-center rounded-md text-sm font-medium text-white bg-[#18B9AE] px-5 py-2 rounded-full"
>
Перейти в настройки аккаунта
</Link>
</Section>
<Section className="text-center mt-8">
<Text className="text-gray-600">
Если у вас возникли вопросы, обращайтесь в службу поддержки по адресу
{' '}
<Link
href="mailto:help@teastream.ru"
className="text-[#18b9ae] underline"
>
help@teastream.ru
</Link>
.
</Text>
</Section>
</Body>
</Tailwind>
</Html>
)
}

View File

@ -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'

View File

@ -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 (
<Html>
<Head />
<Preview>Ваш канал верифицирован</Preview>
<Tailwind>
<Body className="max-w-2xl mx-auto p-6 bg-slate-50">
<Section className="text-center mb-8">
<Heading className="text-3xl text-black font-bold">
Поздравляем! Ваш канал верифицирован
</Heading>
<Text className="text-black text-base mt-2">
Мы рады сообщить, что ваш канал теперь верифицирован, и вы получили официальный значок.
</Text>
</Section>
<Section className="bg-white rounded-lg shadow-md p-6 text-center mb-6">
<Heading className="text-2xl text-black font-semibold">
Что это значит?
</Heading>
<Text className="text-base text-black mt-2">
Значок верификации подтверждает подлинность вашего канала и улучшает доверие зрителей.
</Text>
</Section>
<Section className="text-center mt-8">
<Text className="text-gray-600">
Если у вас есть вопросы, напишите нам на
{' '}
<Link
href="mailto:help@teastream.ru"
className="text-[#18b9ae] underline"
>
help@teastream.ru
</Link>
.
</Text>
</Section>
</Body>
</Tailwind>
</Html>
)
}

View File

@ -62,6 +62,7 @@ export const MESSAGES = {
`<b>📡 На канале ${channel.displayName} началась трансляция!</b>\n\n`
+ `Смотрите здесь: <a href="https://teastream.ru/${channel.name}">Перейти к трансляции</a>`,
newFollowing: (follower: User, followersCount: number) =>
`<b>У вас новый подписчик!</b>\n\nЭто пользователь <a href="https://teastream.ru/${follower.name}">${follower.displayName}</a>\n\nИтоговое количество подписчиков на вашем канале: ${followersCount}`,
enableTwoFactor:
`🔐 Обеспечьте свою безопасность!\n\n`

View File

@ -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 },