From 20191eb35d6881656a78e143837945afdb3a479f Mon Sep 17 00:00:00 2001 From: salvacybersec Date: Mon, 10 Nov 2025 17:27:19 +0300 Subject: [PATCH] feat: Mail template management UI and API CRUD - Added full CRUD endpoints for mail templates (create, update, delete, preview) - Introduced Joi validators for template create/update/preview - Updated routes/controller to support ID and type lookups - Built React Templates page with HTML editor, preview, and clipboard helpers - Added navigation entry and route for /templates - Enhanced documentation (README, QUICKSTART, KULLANIM, frontend/backend README) --- KULLANIM.md | 11 + QUICKSTART.md | 8 + README.md | 8 +- backend/README.md | 10 +- .../src/controllers/template.controller.js | 91 +++- backend/src/routes/template.routes.js | 13 +- backend/src/validators/template.validator.js | 63 +++ frontend/README.md | 13 +- frontend/src/App.jsx | 2 + frontend/src/components/Layout/Layout.jsx | 2 + frontend/src/pages/Templates.jsx | 453 ++++++++++++++++++ 11 files changed, 661 insertions(+), 13 deletions(-) create mode 100644 backend/src/validators/template.validator.js create mode 100644 frontend/src/pages/Templates.jsx 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ı + + + + + + + + Şablon Adı + Template Type + Durum + Güncelleme + İşlemler + + + + {templates.map((template) => ( + + + {template.name} + {template.description && ( + + {template.description} + + )} + + + + + + + + + + + + {format(new Date(template.updated_at), 'dd/MM/yyyy HH:mm')} + + + + + + + + ))} + +
+
+ + + + {selectedTemplate ? 'Şablonu Düzenle' : 'Yeni Şablon Oluştur'} + + + setActiveTab(value)} + sx={{ mb: 2 }} + > + + + + + {activeTab === 'form' && ( + + setForm({ ...form, name: e.target.value })} + /> + setForm({ ...form, template_type: e.target.value.trim().toLowerCase() })} + /> + setForm({ ...form, subject_template: e.target.value })} + /> + setForm({ ...form, description: e.target.value })} + /> + setForm({ ...form, body_html: e.target.value })} + helperText="Handlebars değişkenleri: {{company_name}}, {{employee_name}}, {{tracking_url}}" + /> + setForm({ ...form, active: e.target.checked })} + /> + } + label="Aktif" + /> + + )} + + {activeTab === 'preview' && ( + + + setCompanyPlaceholder(e.target.value)} + /> + setEmployeePlaceholder(e.target.value)} + /> + + + + {previewHtml ? ( +