diff --git a/KULLANIM.md b/KULLANIM.md
index 89cd4b9..5006a95 100644
--- a/KULLANIM.md
+++ b/KULLANIM.md
@@ -123,6 +123,17 @@
## 📧 Mail Şablonları
+### Şablon Paneli (Yeni)
+
+1. **Menü:** Sol menüde `Mail Şablonları` sekmesine tıklayın
+2. **Liste:** Aktif/pasif tüm şablonları görün, güncelleme tarihlerini takip edin
+3. **Oluştur:** `Yeni Şablon` butonu ile HTML içeriği girebilir, template type tanımlayabilirsiniz
+4. **Düzenle:** Her satırdaki `Düzenle` butonu ile şablon bilgilerini güncelleyin
+5. **Önizleme:** `Önizleme` butonu ile şablonu gerçek verilerle render edip tam ekran görüntüleyin
+6. **Sil:** Gereksiz şablonları `Sil` butonu ile kaldırın (tokenlarda kullanılanları değiştirmeyi unutmayın)
+7. **Template Type Kopyalama:** Token oluştururken kullanmak için `Template Type` değerini kopyalayın
+8. **Handlebars Değişkenleri:** Editörde `{{company_name}}`, `{{employee_name}}`, `{{tracking_url}}` değişkenlerini kullanın
+
### Mevcut Şablonlar
**1. Banka Bildirimi (`bank`)**
diff --git a/QUICKSTART.md b/QUICKSTART.md
index a948c60..b8acd53 100644
--- a/QUICKSTART.md
+++ b/QUICKSTART.md
@@ -95,6 +95,14 @@ Artık sistemin tüm özellikleri kullanıma hazır:
- ✅ Telegram bildirimleri
- ✅ Detaylı istatistikler
+## ✉️ Şablonları Özelleştir
+
+1. **Mail Şablonları** sayfasına git
+2. `Yeni Şablon` ile kendi senaryonu oluştur
+3. HTML alanında `{{company_name}}`, `{{employee_name}}`, `{{tracking_url}}` değişkenlerini kullan
+4. `Önizleme` butonuyla gerçek örnek verilerle testi gör
+5. Token oluştururken yeni template type'ı seçmeyi unutma
+
---
## 📚 Daha Fazla Bilgi
diff --git a/README.md b/README.md
index abe3343..128896f 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,7 @@ Güvenlik farkındalık eğitimleri için basit ve etkili phishing test yönetim
- 📊 **Detaylı İstatistikler** - IP, konum, cihaz bilgileri
- 💾 **SQLite** - Tek dosya, kolay yedekleme
- 🎨 **Modern UI** - React ile responsive admin paneli
+- ✉️ **Mail Şablonları** - HTML editör ve önizleme paneli
## 🚀 Hızlı Başlangıç
@@ -54,7 +55,7 @@ oltalama/
│ ├── src/
│ │ ├── services/ ✅ 5 servis (auth, company, token, stats, template)
│ │ ├── context/ ✅ Auth context
-│ │ ├── pages/ ✅ 5 sayfa (Login, Dashboard, Companies, Tokens, Settings)
+│ │ ├── pages/ ✅ 6 sayfa (Login, Dashboard, Companies, Tokens, Templates, Settings)
│ │ └── components/ ✅ Layout + Navigation
└── devpan.md ✅ Detaylı plan
```
@@ -163,8 +164,9 @@ curl http://localhost:3000/api/stats/dashboard
**Core Pages:**
- ✅ Login (Session-based auth)
- ✅ Dashboard (Stats, recent clicks)
-- ✅ Companies (CRUD, grid view)
-- ✅ Tokens (Create & send, table view)
+- ✅ Companies (CRUD, grid view + detail)
+- ✅ Tokens (Create & send, detail & history)
+- ✅ Mail Şablonları (HTML editör + önizleme)
- ✅ Settings (Gmail, Telegram config)
**Components:**
diff --git a/backend/README.md b/backend/README.md
index 630c8ac..4323b20 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -65,9 +65,13 @@ GET /t/:token - Tracking endpoint (IP, GeoIP, Telegram)
### Templates
```
-GET /api/templates - Tüm şablonlar
-GET /api/templates/:type - Şablon detay
-POST /api/templates/preview - Önizleme
+GET /api/templates - Tüm şablonlar
+POST /api/templates - Yeni şablon oluştur
+GET /api/templates/:id - Şablon detay (ID)
+PUT /api/templates/:id - Şablon güncelle
+DELETE /api/templates/:id - Şablon sil
+GET /api/templates/type/:type - Şablon (type ile)
+POST /api/templates/preview - Önizleme
```
### Settings
diff --git a/backend/src/controllers/template.controller.js b/backend/src/controllers/template.controller.js
index 84cc53a..fc23a31 100644
--- a/backend/src/controllers/template.controller.js
+++ b/backend/src/controllers/template.controller.js
@@ -1,6 +1,11 @@
const { MailTemplate } = require('../models');
const mailService = require('../services/mail.service');
+const buildTemplateResponse = (template) => ({
+ success: true,
+ data: template,
+});
+
// Get all templates
exports.getAllTemplates = async (req, res, next) => {
try {
@@ -17,6 +22,25 @@ exports.getAllTemplates = async (req, res, next) => {
}
};
+// Get template by ID
+exports.getTemplateById = async (req, res, next) => {
+ try {
+ const { id } = req.params;
+ const template = await MailTemplate.findByPk(id);
+
+ if (!template) {
+ return res.status(404).json({
+ success: false,
+ error: 'Template not found',
+ });
+ }
+
+ res.json(buildTemplateResponse(template));
+ } catch (error) {
+ next(error);
+ }
+};
+
// Get template by type
exports.getTemplateByType = async (req, res, next) => {
try {
@@ -33,9 +57,71 @@ exports.getTemplateByType = async (req, res, next) => {
});
}
+ res.json(buildTemplateResponse(template));
+ } catch (error) {
+ next(error);
+ }
+};
+
+// Create template
+exports.createTemplate = async (req, res, next) => {
+ try {
+ const template = await MailTemplate.create(req.body);
+ res.status(201).json(buildTemplateResponse(template));
+ } catch (error) {
+ if (error.name === 'SequelizeUniqueConstraintError') {
+ return res.status(400).json({
+ success: false,
+ error: 'Bu template_type değeri zaten kullanılıyor.',
+ });
+ }
+ next(error);
+ }
+};
+
+// Update template
+exports.updateTemplate = async (req, res, next) => {
+ try {
+ const { id } = req.params;
+
+ const template = await MailTemplate.findByPk(id);
+ if (!template) {
+ return res.status(404).json({
+ success: false,
+ error: 'Template not found',
+ });
+ }
+
+ await template.update(req.body);
+ res.json(buildTemplateResponse(template));
+ } catch (error) {
+ if (error.name === 'SequelizeUniqueConstraintError') {
+ return res.status(400).json({
+ success: false,
+ error: 'Bu template_type değeri zaten kullanılıyor.',
+ });
+ }
+ next(error);
+ }
+};
+
+// Delete template
+exports.deleteTemplate = async (req, res, next) => {
+ try {
+ const { id } = req.params;
+
+ const template = await MailTemplate.findByPk(id);
+ if (!template) {
+ return res.status(404).json({
+ success: false,
+ error: 'Template not found',
+ });
+ }
+
+ await template.destroy();
res.json({
success: true,
- data: template,
+ message: 'Template deleted successfully',
});
} catch (error) {
next(error);
@@ -68,5 +154,4 @@ exports.previewTemplate = async (req, res, next) => {
}
};
-module.exports = exports;
-
+module.exports = exports;
\ No newline at end of file
diff --git a/backend/src/routes/template.routes.js b/backend/src/routes/template.routes.js
index faa4dbd..000ad84 100644
--- a/backend/src/routes/template.routes.js
+++ b/backend/src/routes/template.routes.js
@@ -2,13 +2,22 @@ const express = require('express');
const router = express.Router();
const templateController = require('../controllers/template.controller');
const { requireAuth } = require('../middlewares/auth');
+const {
+ validateCreateTemplate,
+ validateUpdateTemplate,
+ validatePreviewTemplate,
+} = require('../validators/template.validator');
// All template routes require authentication
router.use(requireAuth);
router.get('/', templateController.getAllTemplates);
-router.get('/:type', templateController.getTemplateByType);
-router.post('/preview', templateController.previewTemplate);
+router.post('/', validateCreateTemplate, templateController.createTemplate);
+router.get('/:id(\\d+)', templateController.getTemplateById);
+router.put('/:id(\\d+)', validateUpdateTemplate, templateController.updateTemplate);
+router.delete('/:id(\\d+)', templateController.deleteTemplate);
+router.get('/type/:type', templateController.getTemplateByType);
+router.post('/preview', validatePreviewTemplate, templateController.previewTemplate);
module.exports = router;
diff --git a/backend/src/validators/template.validator.js b/backend/src/validators/template.validator.js
new file mode 100644
index 0000000..7062234
--- /dev/null
+++ b/backend/src/validators/template.validator.js
@@ -0,0 +1,63 @@
+const Joi = require('joi');
+
+const baseSchema = {
+ name: Joi.string().max(255).required(),
+ template_type: Joi.string().max(50).required(),
+ subject_template: Joi.string().max(500).allow('', null),
+ body_html: Joi.string().required(),
+ description: Joi.string().allow('', null),
+ active: Joi.boolean().default(true),
+};
+
+const createSchema = Joi.object(baseSchema);
+
+const updateSchema = Joi.object({
+ ...baseSchema,
+});
+
+const previewSchema = Joi.object({
+ template_html: Joi.string().required(),
+ company_name: Joi.string().allow('', null),
+ employee_name: Joi.string().allow('', null),
+});
+
+exports.validateCreateTemplate = (req, res, next) => {
+ const { error, value } = createSchema.validate(req.body, { abortEarly: false });
+ if (error) {
+ return res.status(400).json({
+ success: false,
+ error: 'Validation failed',
+ details: error.details.map((detail) => detail.message),
+ });
+ }
+ req.body = value;
+ next();
+};
+
+exports.validateUpdateTemplate = (req, res, next) => {
+ const { error, value } = updateSchema.validate(req.body, { abortEarly: false });
+ if (error) {
+ return res.status(400).json({
+ success: false,
+ error: 'Validation failed',
+ details: error.details.map((detail) => detail.message),
+ });
+ }
+ req.body = value;
+ next();
+};
+
+exports.validatePreviewTemplate = (req, res, next) => {
+ const { error, value } = previewSchema.validate(req.body, { abortEarly: false });
+ if (error) {
+ return res.status(400).json({
+ success: false,
+ error: 'Validation failed',
+ details: error.details.map((detail) => detail.message),
+ });
+ }
+ req.body = value;
+ next();
+};
+
+
diff --git a/frontend/README.md b/frontend/README.md
index 0649c17..2379c02 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -8,7 +8,7 @@ Modern ve responsive phishing test yönetim paneli frontend'i.
- ⚡ **Vite** - Hızlı development server
- 🔐 **Session Auth** - Context-based authentication
- 📱 **Responsive** - Mobile-first tasarım
-- 🎯 **5 Sayfa** - Login, Dashboard, Companies, Tokens, Settings
+- 🎯 **6 Sayfa** - Login, Dashboard, Companies, Tokens, Templates, Settings
## 🚀 Kurulum
@@ -47,6 +47,14 @@ npm run dev
- Token oluştur + mail gönder
- Durum badge'leri (Tıklandı/Bekliyor)
- Tıklama sayısı tracking
+- Detay sayfası (tıklama geçmişi, IP & cihaz)
+
+### ✉️ Mail Şablonları
+- Tüm şablonların listesi (aktif/pasif)
+- HTML editörü + Handlebars değişkenleri
+- Önizleme (iframe) & tam ekran preview
+- Template type kopyalama, aktif/pasif toggle
+- Oluştur / Düzenle / Sil işlemleri
### ⚙️ Settings
- Gmail yapılandırması
@@ -139,7 +147,8 @@ src/
- ✅ Protected routes
- ✅ Dashboard with stats
- ✅ Company management (CRUD)
-- ✅ Token management (CRUD + send)
+- ✅ Token management (CRUD + send + detay)
+- ✅ Mail templates (CRUD + preview)
- ✅ Settings (Gmail + Telegram)
- ✅ Responsive layout
- ✅ Material-UI theming
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 93c6158..8328494 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -8,6 +8,7 @@ import Companies from './pages/Companies';
import CompanyDetail from './pages/CompanyDetail';
import Tokens from './pages/Tokens';
import TokenDetail from './pages/TokenDetail';
+import Templates from './pages/Templates';
import Settings from './pages/Settings';
const theme = createTheme({
@@ -56,6 +57,7 @@ function App() {
} />
} />
} />
+ } />
} />
diff --git a/frontend/src/components/Layout/Layout.jsx b/frontend/src/components/Layout/Layout.jsx
index 4b935c4..df55c09 100644
--- a/frontend/src/components/Layout/Layout.jsx
+++ b/frontend/src/components/Layout/Layout.jsx
@@ -25,6 +25,7 @@ import {
Settings,
Logout,
AccountCircle,
+ Email,
} from '@mui/icons-material';
import { useAuth } from '../../context/AuthContext';
@@ -34,6 +35,7 @@ const menuItems = [
{ text: 'Dashboard', icon: , path: '/' },
{ text: 'Şirketler', icon: , path: '/companies' },
{ text: 'Tokenlar', icon: , path: '/tokens' },
+ { text: 'Mail Şablonları', icon: , path: '/templates' },
{ text: 'Ayarlar', icon: , path: '/settings' },
];
diff --git a/frontend/src/pages/Templates.jsx b/frontend/src/pages/Templates.jsx
new file mode 100644
index 0000000..70d2817
--- /dev/null
+++ b/frontend/src/pages/Templates.jsx
@@ -0,0 +1,453 @@
+import { useState, useEffect } from 'react';
+import {
+ Box,
+ Button,
+ Paper,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Chip,
+ CircularProgress,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ TextField,
+ Switch,
+ FormControlLabel,
+ Tabs,
+ Tab,
+} from '@mui/material';
+import {
+ Add,
+ Edit,
+ Delete,
+ Preview,
+ ContentCopy,
+} from '@mui/icons-material';
+import { templateService } from '../services/templateService';
+import { format } from 'date-fns';
+
+const defaultForm = {
+ name: '',
+ template_type: '',
+ subject_template: '',
+ body_html: '',
+ description: '',
+ active: true,
+};
+
+function Templates() {
+ const [templates, setTemplates] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [form, setForm] = useState(defaultForm);
+ const [activeTab, setActiveTab] = useState('form');
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [previewOpen, setPreviewOpen] = useState(false);
+ const [previewHtml, setPreviewHtml] = useState('');
+ const [previewUrl, setPreviewUrl] = useState('');
+ const [previewLoading, setPreviewLoading] = useState(false);
+ const [selectedTemplate, setSelectedTemplate] = useState(null);
+ const [companyPlaceholder, setCompanyPlaceholder] = useState('Örnek Şirket');
+ const [employeePlaceholder, setEmployeePlaceholder] = useState('Ahmet Yılmaz');
+
+ useEffect(() => {
+ loadTemplates();
+ }, []);
+
+ const loadTemplates = async () => {
+ try {
+ setLoading(true);
+ const response = await templateService.getAll();
+ setTemplates(response.data);
+ } catch (error) {
+ console.error('Failed to load templates:', error);
+ alert('Şablonlar yüklenemedi');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleOpenCreate = () => {
+ setSelectedTemplate(null);
+ setForm(defaultForm);
+ setActiveTab('form');
+ setDialogOpen(true);
+ };
+
+ const handleOpenEdit = async (template) => {
+ setSelectedTemplate(template);
+ setForm({
+ name: template.name,
+ template_type: template.template_type,
+ subject_template: template.subject_template || '',
+ body_html: template.body_html,
+ description: template.description || '',
+ active: Boolean(template.active),
+ });
+ setActiveTab('form');
+ setDialogOpen(true);
+ };
+
+ const handleCloseDialog = () => {
+ setDialogOpen(false);
+ setSelectedTemplate(null);
+ if (previewUrl) {
+ URL.revokeObjectURL(previewUrl);
+ setPreviewUrl('');
+ }
+ setPreviewHtml('');
+ setActiveTab('form');
+ };
+
+ const handleSave = async () => {
+ try {
+ if (selectedTemplate) {
+ await templateService.update(selectedTemplate.id, form);
+ alert('Şablon güncellendi');
+ } else {
+ await templateService.create(form);
+ alert('Şablon oluşturuldu');
+ }
+ handleCloseDialog();
+ loadTemplates();
+ } catch (error) {
+ const message = error.response?.data?.error || 'Şablon kaydedilemedi';
+ alert(message);
+ console.error('Failed to save template:', error);
+ }
+ };
+
+ const handleDelete = async (template) => {
+ if (!window.confirm(`${template.name} şablonunu silmek istediğinizden emin misiniz?`)) {
+ return;
+ }
+ try {
+ await templateService.delete(template.id);
+ alert('Şablon silindi');
+ loadTemplates();
+ } catch (error) {
+ const message = error.response?.data?.error || 'Şablon silinemedi';
+ alert(message);
+ console.error('Failed to delete template:', error);
+ }
+ };
+
+ const handlePreview = async (htmlOverride, options = { openModal: false }) => {
+ try {
+ setPreviewLoading(true);
+ const htmlContent = htmlOverride ?? form.body_html;
+ if (!htmlContent) {
+ alert('Önizleme için HTML içeriği gerekli');
+ return;
+ }
+ const response = await templateService.preview({
+ template_html: htmlContent,
+ company_name: companyPlaceholder,
+ employee_name: employeePlaceholder,
+ });
+ const renderedHtml = response.data.data.rendered_html;
+ setPreviewHtml(renderedHtml);
+ if (previewUrl) {
+ URL.revokeObjectURL(previewUrl);
+ }
+ const blobUrl = URL.createObjectURL(new Blob([renderedHtml], { type: 'text/html' }));
+ setPreviewUrl(blobUrl);
+ setActiveTab('preview');
+ if (options.openModal) {
+ setPreviewOpen(true);
+ }
+ } catch (error) {
+ const message = error.response?.data?.error || 'Önizleme oluşturulamadı';
+ alert(message);
+ console.error('Failed to preview template:', error);
+ } finally {
+ setPreviewLoading(false);
+ }
+ };
+
+ const copyTemplateType = (value) => {
+ navigator.clipboard.writeText(value);
+ alert(`Template Type panoya kopyalandı: ${value}`);
+ };
+
+ useEffect(() => {
+ return () => {
+ if (previewUrl) {
+ URL.revokeObjectURL(previewUrl);
+ }
+ };
+ }, [previewUrl]);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ Mail Şablonları
+ } onClick={handleOpenCreate}>
+ Yeni Şablon
+
+
+
+
+
+
+
+ Şablon Adı
+ Template Type
+ Durum
+ Güncelleme
+ İşlemler
+
+
+
+ {templates.map((template) => (
+
+
+ {template.name}
+ {template.description && (
+
+ {template.description}
+
+ )}
+
+
+
+
+ }
+ onClick={() => copyTemplateType(template.template_type)}
+ >
+ Kopyala
+
+
+
+
+
+
+
+ {format(new Date(template.updated_at), 'dd/MM/yyyy HH:mm')}
+
+
+ }
+ onClick={() => {
+ setSelectedTemplate(template);
+ setForm({
+ name: template.name,
+ template_type: template.template_type,
+ subject_template: template.subject_template || '',
+ body_html: template.body_html,
+ description: template.description || '',
+ active: Boolean(template.active),
+ });
+ setCompanyPlaceholder('Örnek Şirket');
+ setEmployeePlaceholder('Ahmet Yılmaz');
+ setActiveTab('preview');
+ handlePreview(template.body_html, { openModal: true });
+ }}
+ sx={{ mr: 1 }}
+ >
+ Önizleme
+
+ }
+ onClick={() => handleOpenEdit(template)}
+ sx={{ mr: 1 }}
+ >
+ Düzenle
+
+ }
+ onClick={() => handleDelete(template)}
+ >
+ Sil
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
+
+export default Templates;
+
+