diff --git a/package.json b/package.json index df33e3d..1df50ac 100644 --- a/package.json +++ b/package.json @@ -64,9 +64,13 @@ "@opentelemetry/sdk-trace-node": "^2.8.0", "@opentelemetry/semantic-conventions": "^1.41.1", "@prisma/client": "^7.8.0", + "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^8.0.1", "@types/react": "^19.2.17", "axios": "^1.17.0", "ethers": "^6.17.0", + "node-cron": "^4.5.0", + "nodemailer": "^9.0.1", "pg": "^8.21.0", "react": "^19.2.7", "typeorm": "^1.0.0", diff --git a/src/modules/reports/dto/report.dto.ts b/src/modules/reports/dto/report.dto.ts new file mode 100644 index 0000000..45cce52 --- /dev/null +++ b/src/modules/reports/dto/report.dto.ts @@ -0,0 +1,5 @@ +export interface ReportDto { + title: string; + generatedAt: Date; + sections: { heading: string; body: string }[]; +} diff --git a/src/modules/reports/email.service.ts b/src/modules/reports/email.service.ts new file mode 100644 index 0000000..90cf5fa --- /dev/null +++ b/src/modules/reports/email.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import * as nodemailer from 'nodemailer'; + +@Injectable() +export class EmailService { + private transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: Number(process.env.SMTP_PORT), + secure: false, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS, + }, + }); + + async sendReport(content: string, subject: string) { + await this.transporter.sendMail({ + from: `Security Reports <${process.env.SMTP_USER}>`, + to: process.env.REPORT_RECIPIENT, + subject, + text: content, + }); + } +} diff --git a/src/modules/reports/report.service.spec.ts b/src/modules/reports/report.service.spec.ts new file mode 100644 index 0000000..8006348 --- /dev/null +++ b/src/modules/reports/report.service.spec.ts @@ -0,0 +1,48 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ReportService } from './report.service'; +import { EmailService } from './email.service'; + +describe('ReportService', () => { + let service: ReportService; + let emailService: EmailService; + + beforeEach(async () => { + const mockEmailService = { + sendReport: jest.fn().mockResolvedValue(undefined), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ReportService, + { + provide: EmailService, + useValue: mockEmailService, + }, + ], + }).compile(); + + service = module.get(ReportService); + emailService = module.get(EmailService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should generate daily report content', async () => { + const report = await service.generateDailyReport(); + expect(report).toContain('Daily security summary placeholder'); + }); + + it('should generate weekly report content', async () => { + const report = await service.generateWeeklyReport(); + expect(report).toContain('Weekly security summary placeholder'); + }); + + it('should deliver report via EmailService', async () => { + const content = 'Test Content'; + const subject = 'Test Subject'; + await service.deliverReport(content, subject); + expect(emailService.sendReport).toHaveBeenCalledWith(content, subject); + }); +}); diff --git a/src/modules/reports/report.service.ts b/src/modules/reports/report.service.ts new file mode 100644 index 0000000..7861fde --- /dev/null +++ b/src/modules/reports/report.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { EmailService } from './email.service'; + +@Injectable() +export class ReportService { + constructor(private readonly emailService: EmailService) {} + + async generateDailyReport(): Promise { + // TODO: aggregate daily monitoring data + return 'Daily security summary placeholder'; + } + + async generateWeeklyReport(): Promise { + // TODO: aggregate weekly monitoring data + return 'Weekly security summary placeholder'; + } + + async deliverReport(content: string, subject: string) { + await this.emailService.sendReport(content, subject); + } +} diff --git a/src/modules/reports/reports.module.ts b/src/modules/reports/reports.module.ts new file mode 100644 index 0000000..4d2ed92 --- /dev/null +++ b/src/modules/reports/reports.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ReportService } from './report.service'; +import { SchedulerService } from './scheduler.service'; +import { EmailService } from './email.service'; + +@Module({ + providers: [ReportService, SchedulerService, EmailService], + exports: [ReportService, SchedulerService], +}) +export class ReportsModule {} diff --git a/src/modules/reports/scheduler.service.ts b/src/modules/reports/scheduler.service.ts new file mode 100644 index 0000000..f037d62 --- /dev/null +++ b/src/modules/reports/scheduler.service.ts @@ -0,0 +1,22 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { ReportService } from './report.service'; +import * as cron from 'node-cron'; + +@Injectable() +export class SchedulerService implements OnModuleInit { + constructor(private readonly reportService: ReportService) {} + + onModuleInit() { + // Daily at 08:00 UTC + cron.schedule('0 8 * * *', async () => { + const content = await this.reportService.generateDailyReport(); + await this.reportService.deliverReport(content, 'Daily Security Report'); + }); + + // Weekly on Monday at 09:00 UTC + cron.schedule('0 9 * * MON', async () => { + const content = await this.reportService.generateWeeklyReport(); + await this.reportService.deliverReport(content, 'Weekly Security Report'); + }); + } +}