Skip to content

Commit a5d541b

Browse files
Merge pull request #1018 from portableDD/feat-876-877-879-880
[BE-03/BE-04/BE-06/BE-07] Fix health route, BullMQ, S3, Nodemailer
2 parents 135acfe + 46ceef5 commit a5d541b

11 files changed

Lines changed: 229 additions & 9 deletions

backend/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ SMTP_PASS=
2626
ASSET_ID_PREFIX=AST
2727
ASSET_ID_START=1000
2828

29+
# AWS S3 Configuration
30+
AWS_REGION=us-east-1
31+
AWS_ACCESS_KEY_ID=
32+
AWS_SECRET_ACCESS_KEY=
33+
AWS_S3_BUCKET=assetsup-uploads
34+
2935
# Caching Configuration
3036
REDIS_HOST=localhost
3137
REDIS_PORT=6379

backend/src/app.controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ export class AppController {
1010
return this.appService.getHello();
1111
}
1212

13-
@Get()
13+
@Get('health')
1414
getHealth() {
1515
return {
1616
status: 'OK',
1717
message: 'Server is running',
18-
Timestamp: Date.now(),
18+
timestamp: Date.now(),
1919
};
2020
}
2121
}

backend/src/app.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { AppController } from './app.controller';
77
import { AppService } from './app.service';
88
import { UsersModule } from './users/users.module';
99
import { AuthModule } from './auth/auth.module';
10+
import { QueueModule } from './queue/queue.module';
11+
import { StorageModule } from './storage/storage.module';
1012
import { CacheService } from './cache/cache.service';
1113

1214
@Module({
@@ -56,6 +58,8 @@ import { CacheService } from './cache/cache.service';
5658
},
5759
}),
5860

61+
QueueModule,
62+
StorageModule,
5963
UsersModule,
6064
AuthModule,
6165
],

backend/src/mail/mail.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Module } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
23
import { MailService } from './mail.service';
34

45
@Module({
6+
imports: [ConfigModule],
57
providers: [MailService],
68
exports: [MailService],
79
})

backend/src/mail/mail.service.ts

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,79 @@
11
import { Injectable, Logger } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import * as nodemailer from 'nodemailer';
24

35
@Injectable()
46
export class MailService {
57
private readonly logger = new Logger(MailService.name);
8+
private readonly transporter: nodemailer.Transporter;
9+
10+
constructor(private readonly configService: ConfigService) {
11+
this.transporter = nodemailer.createTransport({
12+
host: configService.get<string>('SMTP_HOST', ''),
13+
port: parseInt(configService.get<string>('SMTP_PORT', '587'), 10),
14+
secure: configService.get<string>('SMTP_SECURE', 'false') === 'true',
15+
auth: {
16+
user: configService.get<string>('SMTP_USER', ''),
17+
pass: configService.get<string>('SMTP_PASS', ''),
18+
},
19+
});
20+
}
621

722
async sendPasswordResetEmail(email: string, resetLink: string) {
8-
this.logger.log(`[MailService] Sending password reset email to ${email}. Link: ${resetLink}`);
9-
// Inn a real application, implement NodeMailer or similar here.
23+
await this.sendTemplateEmail(email, 'Password Reset', 'password-reset', { resetLink });
24+
}
25+
26+
async sendWelcomeEmail(email: string, name: string) {
27+
await this.sendTemplateEmail(email, 'Welcome to AssetsUp', 'welcome', { name });
28+
}
29+
30+
async sendTemplateEmail(to: string, subject: string, template: string, context: Record<string, any>) {
31+
const html = this.renderTemplate(template, context);
32+
try {
33+
await this.transporter.sendMail({
34+
from: this.configService.get<string>('MAIL_FROM', 'noreply@assetsup.local'),
35+
to,
36+
subject,
37+
html,
38+
});
39+
this.logger.log(`Email sent to ${to} with subject "${subject}"`);
40+
} catch (error) {
41+
this.logger.error(`Failed to send email to ${to}: ${error.message}`);
42+
}
43+
}
44+
45+
private renderTemplate(template: string, context: Record<string, any>): string {
46+
const templates: Record<string, (ctx: Record<string, any>) => string> = {
47+
'password-reset': (ctx) => `
48+
<h1>Password Reset</h1>
49+
<p>Click the link below to reset your password:</p>
50+
<a href="${ctx.resetLink}">${ctx.resetLink}</a>
51+
<p>This link expires in 1 hour.</p>
52+
`,
53+
'welcome': (ctx) => `
54+
<h1>Welcome to AssetsUp!</h1>
55+
<p>Hello ${ctx.name},</p>
56+
<p>Your account has been created successfully.</p>
57+
`,
58+
'asset-checkout': (ctx) => `
59+
<h1>Asset Checked Out</h1>
60+
<p>Asset <strong>${ctx.assetName}</strong> has been checked out to ${ctx.assignedTo}.</p>
61+
<p>Due date: ${ctx.dueDate}</p>
62+
`,
63+
'maintenance-due': (ctx) => `
64+
<h1>Maintenance Due</h1>
65+
<p>Asset <strong>${ctx.assetName}</strong> has scheduled maintenance due on ${ctx.dueDate}.</p>
66+
`,
67+
'warranty-expiry': (ctx) => `
68+
<h1>Warranty Expiry Notice</h1>
69+
<p>The warranty for <strong>${ctx.assetName}</strong> expires on ${ctx.expiryDate}.</p>
70+
`,
71+
};
72+
const render = templates[template];
73+
if (!render) {
74+
this.logger.warn(`Unknown email template: ${template}`);
75+
return `<p>${subject}</p>`;
76+
}
77+
return render(context);
1078
}
1179
}
12-
// async sendPasswordResetEmail(email: string, resetLink: string) {
13-
// this.logger.log(`[MailService] Sending password reset email to ${email}. Link: ${resetLink}`);
14-
// // Inn a real application, implement NodeMailer or similar here.
15-
// }
16-
// }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Processor, Process } from '@nestjs/bull';
2+
import { Job } from 'bull';
3+
import { Logger } from '@nestjs/common';
4+
import { MailService } from '../../mail/mail.service';
5+
6+
@Processor('email')
7+
export class EmailProcessor {
8+
private readonly logger = new Logger(EmailProcessor.name);
9+
10+
constructor(private readonly mailService: MailService) {}
11+
12+
@Process('send')
13+
async handleSend(job: Job<{ to: string; subject: string; template: string; context: Record<string, any> }>) {
14+
this.logger.log(`Processing email job #${job.id} to ${job.data.to}`);
15+
await this.mailService.sendTemplateEmail(job.data.to, job.data.subject, job.data.template, job.data.context);
16+
}
17+
}

backend/src/queue/queue.module.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Module } from '@nestjs/common';
2+
import { BullModule } from '@nestjs/bull';
3+
import { ConfigModule, ConfigService } from '@nestjs/config';
4+
import { QueueService } from './queue.service';
5+
import { EmailProcessor } from './processors/email.processor';
6+
7+
@Module({
8+
imports: [
9+
BullModule.forRootAsync({
10+
imports: [ConfigModule],
11+
inject: [ConfigService],
12+
useFactory: (configService: ConfigService) => ({
13+
redis: {
14+
host: configService.get<string>('REDIS_HOST', 'localhost'),
15+
port: parseInt(configService.get<string>('REDIS_PORT', '6379'), 10),
16+
},
17+
}),
18+
}),
19+
BullModule.registerQueue({
20+
name: 'email',
21+
}),
22+
],
23+
providers: [QueueService, EmailProcessor],
24+
exports: [QueueService, BullModule],
25+
})
26+
export class QueueModule {}

backend/src/queue/queue.service.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { InjectQueue } from '@nestjs/bull';
3+
import { Queue } from 'bull';
4+
5+
@Injectable()
6+
export class QueueService {
7+
constructor(
8+
@InjectQueue('email') private readonly emailQueue: Queue,
9+
) {}
10+
11+
async sendEmail(data: { to: string; subject: string; template: string; context: Record<string, any> }): Promise<void> {
12+
await this.emailQueue.add('send', data, {
13+
attempts: 3,
14+
backoff: { type: 'exponential', delay: 2000 },
15+
});
16+
}
17+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Controller, Post, Delete, Param, UseInterceptors, UploadedFile, Body } from '@nestjs/common';
2+
import { FileInterceptor } from '@nestjs/platform-express';
3+
import { StorageService } from './storage.service';
4+
5+
@Controller('storage')
6+
export class StorageController {
7+
constructor(private readonly storageService: StorageService) {}
8+
9+
@Post('upload')
10+
@UseInterceptors(FileInterceptor('file'))
11+
async uploadFile(@UploadedFile() file: Express.Multer.File) {
12+
const key = await this.storageService.upload(file);
13+
return { key, url: await this.storageService.getSignedUrl(key) };
14+
}
15+
16+
@Delete(':key')
17+
async deleteFile(@Param('key') key: string) {
18+
await this.storageService.delete(key);
19+
return { message: 'File deleted' };
20+
}
21+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
3+
import { StorageService } from './storage.service';
4+
import { StorageController } from './storage.controller';
5+
6+
@Module({
7+
imports: [ConfigModule],
8+
controllers: [StorageController],
9+
providers: [StorageService],
10+
exports: [StorageService],
11+
})
12+
export class StorageModule {}

0 commit comments

Comments
 (0)