Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ SMTP_PASS=
ASSET_ID_PREFIX=AST
ASSET_ID_START=1000

# AWS S3 Configuration
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_S3_BUCKET=assetsup-uploads

# Caching Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
Expand Down
4 changes: 2 additions & 2 deletions backend/src/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ export class AppController {
return this.appService.getHello();
}

@Get()
@Get('health')
getHealth() {
return {
status: 'OK',
message: 'Server is running',
Timestamp: Date.now(),
timestamp: Date.now(),
};
}
}
4 changes: 4 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { QueueModule } from './queue/queue.module';
import { StorageModule } from './storage/storage.module';
import { CacheService } from './cache/cache.service';

@Module({
Expand Down Expand Up @@ -56,6 +58,8 @@ import { CacheService } from './cache/cache.service';
},
}),

QueueModule,
StorageModule,
UsersModule,
AuthModule,
],
Expand Down
2 changes: 2 additions & 0 deletions backend/src/mail/mail.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MailService } from './mail.service';

@Module({
imports: [ConfigModule],
providers: [MailService],
exports: [MailService],
})
Expand Down
77 changes: 70 additions & 7 deletions backend/src/mail/mail.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,79 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as nodemailer from 'nodemailer';

@Injectable()
export class MailService {
private readonly logger = new Logger(MailService.name);
private readonly transporter: nodemailer.Transporter;

constructor(private readonly configService: ConfigService) {
this.transporter = nodemailer.createTransport({
host: configService.get<string>('SMTP_HOST', ''),
port: parseInt(configService.get<string>('SMTP_PORT', '587'), 10),
secure: configService.get<string>('SMTP_SECURE', 'false') === 'true',
auth: {
user: configService.get<string>('SMTP_USER', ''),
pass: configService.get<string>('SMTP_PASS', ''),
},
});
}

async sendPasswordResetEmail(email: string, resetLink: string) {
this.logger.log(`[MailService] Sending password reset email to ${email}. Link: ${resetLink}`);
// Inn a real application, implement NodeMailer or similar here.
await this.sendTemplateEmail(email, 'Password Reset', 'password-reset', { resetLink });
}

async sendWelcomeEmail(email: string, name: string) {
await this.sendTemplateEmail(email, 'Welcome to AssetsUp', 'welcome', { name });
}

async sendTemplateEmail(to: string, subject: string, template: string, context: Record<string, any>) {
const html = this.renderTemplate(template, context);
try {
await this.transporter.sendMail({
from: this.configService.get<string>('MAIL_FROM', 'noreply@assetsup.local'),
to,
subject,
html,
});
this.logger.log(`Email sent to ${to} with subject "${subject}"`);
} catch (error) {
this.logger.error(`Failed to send email to ${to}: ${error.message}`);
}
}

private renderTemplate(template: string, context: Record<string, any>): string {
const templates: Record<string, (ctx: Record<string, any>) => string> = {
'password-reset': (ctx) => `
<h1>Password Reset</h1>
<p>Click the link below to reset your password:</p>
<a href="${ctx.resetLink}">${ctx.resetLink}</a>
<p>This link expires in 1 hour.</p>
`,
'welcome': (ctx) => `
<h1>Welcome to AssetsUp!</h1>
<p>Hello ${ctx.name},</p>
<p>Your account has been created successfully.</p>
`,
'asset-checkout': (ctx) => `
<h1>Asset Checked Out</h1>
<p>Asset <strong>${ctx.assetName}</strong> has been checked out to ${ctx.assignedTo}.</p>
<p>Due date: ${ctx.dueDate}</p>
`,
'maintenance-due': (ctx) => `
<h1>Maintenance Due</h1>
<p>Asset <strong>${ctx.assetName}</strong> has scheduled maintenance due on ${ctx.dueDate}.</p>
`,
'warranty-expiry': (ctx) => `
<h1>Warranty Expiry Notice</h1>
<p>The warranty for <strong>${ctx.assetName}</strong> expires on ${ctx.expiryDate}.</p>
`,
};
const render = templates[template];
if (!render) {
this.logger.warn(`Unknown email template: ${template}`);
return `<p>${subject}</p>`;
}
return render(context);
}
}
// async sendPasswordResetEmail(email: string, resetLink: string) {
// this.logger.log(`[MailService] Sending password reset email to ${email}. Link: ${resetLink}`);
// // Inn a real application, implement NodeMailer or similar here.
// }
// }
17 changes: 17 additions & 0 deletions backend/src/queue/processors/email.processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bull';
import { Logger } from '@nestjs/common';
import { MailService } from '../../mail/mail.service';

@Processor('email')
export class EmailProcessor {
private readonly logger = new Logger(EmailProcessor.name);

constructor(private readonly mailService: MailService) {}

@Process('send')
async handleSend(job: Job<{ to: string; subject: string; template: string; context: Record<string, any> }>) {
this.logger.log(`Processing email job #${job.id} to ${job.data.to}`);
await this.mailService.sendTemplateEmail(job.data.to, job.data.subject, job.data.template, job.data.context);
}
}
26 changes: 26 additions & 0 deletions backend/src/queue/queue.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { QueueService } from './queue.service';
import { EmailProcessor } from './processors/email.processor';

@Module({
imports: [
BullModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
redis: {
host: configService.get<string>('REDIS_HOST', 'localhost'),
port: parseInt(configService.get<string>('REDIS_PORT', '6379'), 10),
},
}),
}),
BullModule.registerQueue({
name: 'email',
}),
],
providers: [QueueService, EmailProcessor],
exports: [QueueService, BullModule],
})
export class QueueModule {}
17 changes: 17 additions & 0 deletions backend/src/queue/queue.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Injectable } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';

@Injectable()
export class QueueService {
constructor(
@InjectQueue('email') private readonly emailQueue: Queue,
) {}

async sendEmail(data: { to: string; subject: string; template: string; context: Record<string, any> }): Promise<void> {
await this.emailQueue.add('send', data, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
});
}
}
21 changes: 21 additions & 0 deletions backend/src/storage/storage.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Controller, Post, Delete, Param, UseInterceptors, UploadedFile, Body } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { StorageService } from './storage.service';

@Controller('storage')
export class StorageController {
constructor(private readonly storageService: StorageService) {}

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
async uploadFile(@UploadedFile() file: Express.Multer.File) {
const key = await this.storageService.upload(file);
return { key, url: await this.storageService.getSignedUrl(key) };
}

@Delete(':key')
async deleteFile(@Param('key') key: string) {
await this.storageService.delete(key);
return { message: 'File deleted' };
}
}
12 changes: 12 additions & 0 deletions backend/src/storage/storage.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { StorageService } from './storage.service';
import { StorageController } from './storage.controller';

@Module({
imports: [ConfigModule],
controllers: [StorageController],
providers: [StorageService],
exports: [StorageService],
})
export class StorageModule {}
52 changes: 52 additions & 0 deletions backend/src/storage/storage.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

@Injectable()
export class StorageService {
private readonly logger = new Logger(StorageService.name);
private readonly s3: S3Client;
private readonly bucket: string;

constructor(private readonly configService: ConfigService) {
this.s3 = new S3Client({
region: configService.get<string>('AWS_REGION', 'us-east-1'),
credentials: {
accessKeyId: configService.get<string>('AWS_ACCESS_KEY_ID', ''),
secretAccessKey: configService.get<string>('AWS_SECRET_ACCESS_KEY', ''),
},
});
this.bucket = configService.get<string>('AWS_S3_BUCKET', 'assetsup-uploads');
}

async upload(file: Express.Multer.File, key?: string): Promise<string> {
const fileKey = key || `${Date.now()}-${file.originalname}`;
await this.s3.send(
new PutObjectCommand({
Bucket: this.bucket,
Key: fileKey,
Body: file.buffer,
ContentType: file.mimetype,
}),
);
return fileKey;
}

async delete(key: string): Promise<void> {
await this.s3.send(
new DeleteObjectCommand({
Bucket: this.bucket,
Key: key,
}),
);
}

async getSignedUrl(key: string, expiresIn = 3600): Promise<string> {
return getSignedUrl(
this.s3,
new GetObjectCommand({ Bucket: this.bucket, Key: key }),
{ expiresIn },
);
}
}
Loading