feat: contact sayfasına iletişim formu + POST /contact endpoint'i
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
- Yeni contact modülü: @Public POST /contact, zod validation, @Throttle 5/10dk spam koruması; EmailService ile admin@sase.tr'ye mail (reply-to = gönderen), kullanıcı girdileri HTML-escape - EmailService: replyTo desteği eklendi - contact.tsx: useState + zod + @sase/ui ile iletişim formu (mevcut form pattern'iyle tutarlı; yeni form kütüphanesi yok), toast + alan validasyonu Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
apps/api/src/contact/contact.controller.ts
Normal file
17
apps/api/src/contact/contact.controller.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Body, Controller, Post } from "@nestjs/common";
|
||||
import { Throttle } from "@nestjs/throttler";
|
||||
import { Public } from "../common/decorators/public.decorator";
|
||||
import { ContactService } from "./contact.service";
|
||||
|
||||
@Controller("contact")
|
||||
export class ContactController {
|
||||
constructor(private readonly contactService: ContactService) {}
|
||||
|
||||
@Post()
|
||||
@Public()
|
||||
// Spam koruması: IP başına 10 dakikada en fazla 5 gönderim.
|
||||
@Throttle({ default: { limit: 5, ttl: 600_000 } })
|
||||
async submit(@Body() body: unknown) {
|
||||
return this.contactService.submit(body);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/contact/contact.dto.ts
Normal file
10
apps/api/src/contact/contact.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const contactMessageSchema = z.object({
|
||||
name: z.string().trim().min(2, "Ad en az 2 karakter olmalı").max(100, "Ad çok uzun"),
|
||||
email: z.string().trim().email("Geçerli bir e-posta adresi girin").max(200),
|
||||
subject: z.string().trim().max(200, "Konu çok uzun").optional().default(""),
|
||||
message: z.string().trim().min(10, "Mesaj en az 10 karakter olmalı").max(5000, "Mesaj çok uzun"),
|
||||
});
|
||||
|
||||
export type ContactMessage = z.infer<typeof contactMessageSchema>;
|
||||
10
apps/api/src/contact/contact.module.ts
Normal file
10
apps/api/src/contact/contact.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ContactController } from "./contact.controller";
|
||||
import { ContactService } from "./contact.service";
|
||||
|
||||
// EmailService @Global EmailModule'den gelir.
|
||||
@Module({
|
||||
controllers: [ContactController],
|
||||
providers: [ContactService],
|
||||
})
|
||||
export class ContactModule {}
|
||||
50
apps/api/src/contact/contact.service.ts
Normal file
50
apps/api/src/contact/contact.service.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
import { EmailService } from "../email/email.service";
|
||||
import { contactMessageSchema } from "./contact.dto";
|
||||
|
||||
// İletişim formu mesajlarının gönderileceği adres.
|
||||
const CONTACT_RECIPIENT = "admin@sase.tr";
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ContactService {
|
||||
private readonly logger = new Logger(ContactService.name);
|
||||
|
||||
constructor(private readonly email: EmailService) {}
|
||||
|
||||
async submit(body: unknown): Promise<{ success: true }> {
|
||||
const parsed = contactMessageSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestException(parsed.error.issues[0]?.message ?? "Geçersiz form verisi");
|
||||
}
|
||||
|
||||
const { name, email, message } = parsed.data;
|
||||
const subject = parsed.data.subject?.trim() || "Yeni mesaj";
|
||||
|
||||
await this.email.send({
|
||||
to: CONTACT_RECIPIENT,
|
||||
replyTo: email, // admin doğrudan gönderene yanıt verebilsin
|
||||
subject: `İletişim Formu: ${subject}`,
|
||||
tag: "contact-form",
|
||||
html: `
|
||||
<h2>Yeni iletişim formu mesajı</h2>
|
||||
<p><strong>Ad:</strong> ${escapeHtml(name)}</p>
|
||||
<p><strong>E-posta:</strong> ${escapeHtml(email)}</p>
|
||||
<p><strong>Konu:</strong> ${escapeHtml(subject)}</p>
|
||||
<p><strong>Mesaj:</strong></p>
|
||||
<p style="white-space: pre-wrap">${escapeHtml(message)}</p>
|
||||
`,
|
||||
text: `Ad: ${name}\nE-posta: ${email}\nKonu: ${subject}\n\nMesaj:\n${message}`,
|
||||
});
|
||||
|
||||
this.logger.log(`Contact form submitted by ${email}`);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user