diff --git a/prisma/migrations/20251209114830_contact_message/migration.sql b/prisma/migrations/20251209114830_contact_message/migration.sql new file mode 100644 index 0000000..f203353 --- /dev/null +++ b/prisma/migrations/20251209114830_contact_message/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "ContactMessage" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "subject" TEXT, + "message" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ContactMessage_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8173e9b..05ea0fe 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -135,3 +135,12 @@ model File { @@index([folder, originalName]) } + +model ContactMessage { + id String @id @default(uuid()) + name String + email String + subject String? + message String + createdAt DateTime @default(now()) +} diff --git a/src/app.module.ts b/src/app.module.ts index 6f473ba..81864f6 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,6 +7,7 @@ import { CategoryModule } from './category/category.module'; import { RestaurantModule } from './restaurant/restaurant.module'; import { ItemModule } from './item/item.module'; import { FileModule } from './file/file.module'; +import { ContactModule } from './contact/contact.module'; @Module({ imports: [ @@ -17,6 +18,7 @@ import { FileModule } from './file/file.module'; RestaurantModule, ItemModule, FileModule, + ContactModule, ], }) export class AppModule {} diff --git a/src/contact/contact.controller.ts b/src/contact/contact.controller.ts new file mode 100644 index 0000000..62f3d21 --- /dev/null +++ b/src/contact/contact.controller.ts @@ -0,0 +1,21 @@ +import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; +import { ContactService } from './contact.service'; +import { AllowAnonymous, AuthGuard } from '@thallesp/nestjs-better-auth'; +import { CreateContactDto } from './dto/createContant.dto'; + +@Controller('contact') +@UseGuards(AuthGuard) +export class ContactController { + constructor(private readonly contactService: ContactService) {} + + @Post() + @AllowAnonymous() + async create(@Body() data: CreateContactDto) { + return this.contactService.create(data); + } + + @Get() + async findAll() { + return this.contactService.findAll(); + } +} diff --git a/src/contact/contact.module.ts b/src/contact/contact.module.ts new file mode 100644 index 0000000..2645f5b --- /dev/null +++ b/src/contact/contact.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ContactService } from './contact.service'; +import { ContactController } from './contact.controller'; +import { PrismaModule } from 'src/prisma/prisma.module'; + +@Module({ + imports: [PrismaModule], + providers: [ContactService], + controllers: [ContactController], +}) +export class ContactModule {} diff --git a/src/contact/contact.service.ts b/src/contact/contact.service.ts new file mode 100644 index 0000000..df72c01 --- /dev/null +++ b/src/contact/contact.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { Prisma } from '@prisma/client'; +import { emailService } from '../shared/services/email.service'; +import { CreateContactDto } from './dto/createContant.dto'; + +@Injectable() +export class ContactService { + constructor(private prisma: PrismaService) {} + + async create(data: CreateContactDto) { + const message = await this.prisma.contactMessage.create({ + data: { + name: data.name, + email: data.email, + subject: data.subject, + message: data.message, + }, + }); + + // Send email notification (fire and forget, or await if critical) + // We'll catch errors so it doesn't fail the request if email fails + try { + await emailService.sendContactMessageNotification({ + name: message.name, + email: message.email, + subject: message.subject || undefined, + message: message.message, + }); + } catch (error) { + console.error('Failed to send contact notification email:', error); + } + + return message; + } + + async findAll() { + return this.prisma.contactMessage.findMany({ + orderBy: { + createdAt: 'desc', + }, + }); + } +} diff --git a/src/contact/dto/createContant.dto.ts b/src/contact/dto/createContant.dto.ts new file mode 100644 index 0000000..65f4204 --- /dev/null +++ b/src/contact/dto/createContant.dto.ts @@ -0,0 +1,22 @@ +import { IsString, IsEmail, IsNotEmpty, IsOptional, MaxLength } from 'class-validator'; + +export class CreateContactDto { + @IsString() + @IsNotEmpty() + @MaxLength(100) + name: string; + + @IsEmail() + @IsNotEmpty() + email: string; + + @IsString() + @IsOptional() + @MaxLength(20) + subject?: string; + + @IsString() + @IsNotEmpty() + @MaxLength(1000) + message: string; +} diff --git a/src/shared/config/config.ts b/src/shared/config/config.ts index 974e310..d555682 100644 --- a/src/shared/config/config.ts +++ b/src/shared/config/config.ts @@ -47,5 +47,8 @@ export const config = { user: process.env.EMAIL_USER, password: process.env.EMAIL_PASSWORD, from: process.env.EMAIL_FROM || '"Zemenu" ', + contactNotificationEmails: process.env.CONTACT_NOTIFICATION_EMAILS + ? process.env.CONTACT_NOTIFICATION_EMAILS.split(',').map(email => email.trim()) + : [process.env.EMAIL_FROM || 'no-reply@zemenu.com'], }, }; diff --git a/src/shared/services/email.service.ts b/src/shared/services/email.service.ts index ad5753c..e70f770 100644 --- a/src/shared/services/email.service.ts +++ b/src/shared/services/email.service.ts @@ -52,6 +52,21 @@ export class EmailService { await this.sendEmail(to, 'Reset your password', html); } + async sendContactMessageNotification(data: { name: string; email: string; subject?: string; message: string }) { + const content = ` +

You have received a new contact message from ${data.name} (${data.email}).

+

Subject: ${data.subject || 'No Subject'}

+
+

${data.message}

+ `; + + const html = this.getHtmlTemplate('New Contact Message', content); + + // Send to the configured notification emails + const recipients = config.email.contactNotificationEmails; + await this.sendEmail(recipients.join(','), `New Contact Message: ${data.subject || 'No Subject'}`, html); + } + private getHtmlTemplate(title: string, content: string): string { const logoUrl = 'https://res.cloudinary.com/dmxoohiwo/image/upload/v1764315105/logo_fpczsd.png'; const currentYear = new Date().getFullYear();