feat: Add Ollama AI integration for automatic mail template generation

 New Features:
- 🤖 AI-powered mail template generation with Ollama
- 📧 Test mail sending with preview
- 🔧 Ollama server and model management
- 🎨 Beautiful AI generation dialog in Templates page
- ⚙️ Ollama settings panel with connection test

Backend:
- Add ollama.service.js - Ollama API integration
- Add ollama.controller.js - Template generation endpoint
- Add ollama.routes.js - /api/ollama/* routes
- Support for multiple Ollama models (llama3.2, mistral, gemma)
- JSON-formatted AI responses with subject + HTML body
- Configurable server URL and model selection

Frontend:
- Settings: Ollama configuration panel
  - Server URL input
  - Model selection
  - Connection test with model listing
- Templates: AI generation dialog
  - Company name, scenario, employee info inputs
  - Custom prompt for AI instructions
  - Auto-save to database
  - Test mail sending functionality

Documentation:
- OLLAMA_SETUP.md - Comprehensive setup guide
- Installation instructions
- Model recommendations
- Usage examples
- Troubleshooting

Tech Stack:
- Ollama API integration (REST)
- Axios HTTP client
- React dialogs with MUI
- Self-hosted AI (privacy-friendly)
- Zero external API dependencies

Example Usage:
  Company: Garanti Bankası
  Scenario: Account security warning
  → AI generates professional phishing test mail in seconds!
This commit is contained in:
salvacybersec
2025-11-10 21:13:58 +03:00
parent d41ff7671e
commit af0510e486
8 changed files with 1121 additions and 7 deletions

View File

@@ -27,10 +27,13 @@ function Settings() {
gmail_from_name: '',
telegram_bot_token: '',
telegram_chat_id: '',
ollama_server_url: '',
ollama_model: '',
});
const [loading, setLoading] = useState(true);
const [testLoading, setTestLoading] = useState({ mail: false, telegram: false });
const [alerts, setAlerts] = useState({ mail: null, telegram: null });
const [testLoading, setTestLoading] = useState({ mail: false, telegram: false, ollama: false });
const [alerts, setAlerts] = useState({ mail: null, telegram: null, ollama: null });
const [ollamaModels, setOllamaModels] = useState([]);
useEffect(() => {
loadSettings();
@@ -60,6 +63,8 @@ function Settings() {
gmail_from_name: settingsObj.gmail_from_name || '',
telegram_bot_token: settingsObj.telegram_bot_token || '',
telegram_chat_id: settingsObj.telegram_chat_id || '',
ollama_server_url: settingsObj.ollama_server_url || '',
ollama_model: settingsObj.ollama_model || '',
});
} catch (error) {
console.error('Failed to load settings:', error);
@@ -85,6 +90,10 @@ function Settings() {
telegram_bot_token: settings.telegram_bot_token,
telegram_chat_id: settings.telegram_chat_id,
}, { withCredentials: true }),
axios.put(`${API_URL}/api/ollama/settings`, {
ollama_server_url: settings.ollama_server_url,
ollama_model: settings.ollama_model,
}, { withCredentials: true }),
]);
alert('Ayarlar kaydedildi!');
} catch (error) {
@@ -134,6 +143,44 @@ function Settings() {
}
};
const handleTestOllama = async () => {
setTestLoading({ ...testLoading, ollama: true });
setAlerts({ ...alerts, ollama: null });
try {
const response = await axios.get(
`${API_URL}/api/ollama/test`,
{ withCredentials: true }
);
if (response.data.success) {
setOllamaModels(response.data.data.models || []);
setAlerts({
...alerts,
ollama: {
severity: 'success',
message: `${response.data.message} - ${response.data.data.models.length} model bulundu`
},
});
} else {
setAlerts({
...alerts,
ollama: { severity: 'error', message: response.data.message },
});
}
} catch (error) {
setAlerts({
...alerts,
ollama: {
severity: 'error',
message: error.response?.data?.error || 'Ollama bağlantısı başarısız',
},
});
} finally {
setTestLoading({ ...testLoading, ollama: false });
}
};
if (loading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
@@ -333,6 +380,80 @@ function Settings() {
</Box>
</Paper>
</Grid>
{/* Ollama Settings */}
<Grid size={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
🤖 Ollama AI Ayarları
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
AI ile mail şablonu oluşturmak için Ollama yapılandırması
</Typography>
<TextField
fullWidth
margin="normal"
label="Ollama Server URL"
type="url"
placeholder="http://localhost:11434"
value={settings.ollama_server_url}
onChange={(e) =>
setSettings({ ...settings, ollama_server_url: e.target.value })
}
helperText="Ollama sunucu adresi (varsayılan: http://localhost:11434)"
/>
<TextField
fullWidth
margin="normal"
label="Model"
placeholder="llama3.2"
value={settings.ollama_model}
onChange={(e) =>
setSettings({ ...settings, ollama_model: e.target.value })
}
helperText="Kullanılacak Ollama model adı (örn: llama3.2, mistral, gemma)"
/>
{alerts.ollama && (
<Alert severity={alerts.ollama.severity} sx={{ mt: 2 }}>
{alerts.ollama.message}
</Alert>
)}
{ollamaModels.length > 0 && (
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2" fontWeight="bold">
Mevcut Modeller:
</Typography>
{ollamaModels.map((model, idx) => (
<Typography key={idx} variant="body2">
{model.name} ({(model.size / 1024 / 1024 / 1024).toFixed(1)} GB)
</Typography>
))}
</Alert>
)}
<Box mt={2} display="flex" gap={2}>
<Button
variant="contained"
startIcon={<Save />}
onClick={handleSave}
>
Kaydet
</Button>
<Button
variant="outlined"
startIcon={<Send />}
onClick={handleTestOllama}
disabled={testLoading.ollama}
>
{testLoading.ollama ? <CircularProgress size={20} /> : 'Bağlantıyı Test Et'}
</Button>
</Box>
</Paper>
</Grid>
</Grid>
<Paper sx={{ p: 3, mt: 3 }}>

View File

@@ -28,9 +28,14 @@ import {
Delete,
Preview,
ContentCopy,
AutoAwesome,
Send as SendIcon,
} from '@mui/icons-material';
import { templateService } from '../services/templateService';
import { format } from 'date-fns';
import axios from 'axios';
const API_URL = import.meta.env.VITE_API_URL;
const defaultForm = {
name: '',
@@ -54,6 +59,18 @@ function Templates() {
const [selectedTemplate, setSelectedTemplate] = useState(null);
const [companyPlaceholder, setCompanyPlaceholder] = useState('Örnek Şirket');
const [employeePlaceholder, setEmployeePlaceholder] = useState('Ahmet Yılmaz');
const [aiDialogOpen, setAiDialogOpen] = useState(false);
const [aiGenerating, setAiGenerating] = useState(false);
const [aiForm, setAiForm] = useState({
company_name: '',
scenario: '',
employee_info: '',
custom_prompt: '',
template_name: '',
template_type: '',
});
const [testMailDialogOpen, setTestMailDialogOpen] = useState(false);
const [testMailAddress, setTestMailAddress] = useState('');
useEffect(() => {
loadTemplates();
@@ -138,6 +155,69 @@ function Templates() {
}
};
const handleGenerateWithAI = async () => {
if (!aiForm.company_name || !aiForm.scenario || !aiForm.template_name || !aiForm.template_type) {
alert('Lütfen tüm zorunlu alanları doldurun');
return;
}
setAiGenerating(true);
try {
const response = await axios.post(
`${API_URL}/api/ollama/generate-template`,
aiForm,
{ withCredentials: true }
);
alert(response.data.message);
setAiDialogOpen(false);
setAiForm({
company_name: '',
scenario: '',
employee_info: '',
custom_prompt: '',
template_name: '',
template_type: '',
});
loadTemplates();
} catch (error) {
const message = error.response?.data?.error || 'AI ile şablon oluşturulamadı';
alert(message);
console.error('AI generation failed:', error);
} finally {
setAiGenerating(false);
}
};
const handleSendTestMail = async () => {
if (!testMailAddress || !selectedTemplate) {
alert('Lütfen mail adresi girin');
return;
}
try {
await axios.post(
`${API_URL}/api/ollama/send-test-mail`,
{
test_email: testMailAddress,
subject: selectedTemplate.subject_template,
body: selectedTemplate.body_html,
company_name: companyPlaceholder,
employee_name: employeePlaceholder,
},
{ withCredentials: true }
);
alert('Test maili gönderildi!');
setTestMailDialogOpen(false);
setTestMailAddress('');
} catch (error) {
const message = error.response?.data?.error || 'Test maili gönderilemedi';
alert(message);
console.error('Test mail failed:', error);
}
};
const handlePreview = async (htmlOverride, options = { openModal: false }) => {
try {
setPreviewLoading(true);
@@ -196,9 +276,19 @@ function Templates() {
<Box>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
<Typography variant="h4">Mail Şablonları</Typography>
<Button variant="contained" startIcon={<Add />} onClick={handleOpenCreate}>
Yeni Şablon
</Button>
<Box display="flex" gap={2}>
<Button
variant="outlined"
startIcon={<AutoAwesome />}
onClick={() => setAiDialogOpen(true)}
color="secondary"
>
AI ile Oluştur
</Button>
<Button variant="contained" startIcon={<Add />} onClick={handleOpenCreate}>
Yeni Şablon
</Button>
</Box>
</Box>
<TableContainer component={Paper}>
@@ -273,6 +363,18 @@ function Templates() {
>
Önizleme
</Button>
<Button
size="small"
color="info"
startIcon={<SendIcon />}
onClick={() => {
setSelectedTemplate(template);
setTestMailDialogOpen(true);
}}
sx={{ mr: 1 }}
>
Test Mail
</Button>
<Button
size="small"
startIcon={<Edit />}
@@ -445,6 +547,153 @@ function Templates() {
</Button>
</DialogActions>
</Dialog>
{/* AI Generation Dialog */}
<Dialog open={aiDialogOpen} onClose={() => setAiDialogOpen(false)} maxWidth="md" fullWidth>
<DialogTitle>🤖 AI ile Mail Şablonu Oluştur</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" gutterBottom>
Ollama AI kullanarak otomatik mail şablonu oluşturun. Aşağıdaki bilgileri girin:
</Typography>
<TextField
fullWidth
margin="normal"
label="Şablon Adı"
required
value={aiForm.template_name}
onChange={(e) => setAiForm({ ...aiForm, template_name: e.target.value })}
helperText="Şablona verilecek isim"
/>
<TextField
fullWidth
margin="normal"
label="Template Type"
required
value={aiForm.template_type}
onChange={(e) => setAiForm({ ...aiForm, template_type: e.target.value })}
helperText="Örn: bank, hr, it_support, management"
/>
<TextField
fullWidth
margin="normal"
label="Şirket Adı"
required
value={aiForm.company_name}
onChange={(e) => setAiForm({ ...aiForm, company_name: e.target.value })}
helperText="Hedef şirket adı (örn: Acme Corporation)"
/>
<TextField
fullWidth
margin="normal"
label="Senaryo"
required
multiline
rows={3}
value={aiForm.scenario}
onChange={(e) => setAiForm({ ...aiForm, scenario: e.target.value })}
helperText="Mail senaryosu (örn: 'Şifre sıfırlama maili', 'Yeni güvenlik politikası', 'Ödül programı duyurusu')"
/>
<TextField
fullWidth
margin="normal"
label="Çalışan Bilgisi (Opsiyonel)"
value={aiForm.employee_info}
onChange={(e) => setAiForm({ ...aiForm, employee_info: e.target.value })}
helperText="Hedef çalışan hakkında bilgi (örn: 'İK departmanı çalışanı', 'Yönetici')"
/>
<TextField
fullWidth
margin="normal"
label="Ek Talimatlar (Opsiyonel)"
multiline
rows={2}
value={aiForm.custom_prompt}
onChange={(e) => setAiForm({ ...aiForm, custom_prompt: e.target.value })}
helperText="AI'ya özel talimatlar (örn: 'Resmi dil kullan', 'Aciliyet vurgusu yap')"
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setAiDialogOpen(false)} disabled={aiGenerating}>
İptal
</Button>
<Button
onClick={handleGenerateWithAI}
variant="contained"
disabled={aiGenerating}
startIcon={aiGenerating ? <CircularProgress size={20} /> : <AutoAwesome />}
>
{aiGenerating ? 'Oluşturuluyor...' : 'Oluştur'}
</Button>
</DialogActions>
</Dialog>
{/* Test Mail Dialog */}
<Dialog open={testMailDialogOpen} onClose={() => setTestMailDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>📧 Test Maili Gönder</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" gutterBottom>
Seçili şablon için test maili gönderin.
</Typography>
{selectedTemplate && (
<Box my={2} p={2} bgcolor="grey.100" borderRadius={1}>
<Typography variant="body2" fontWeight="bold">
Şablon: {selectedTemplate.name}
</Typography>
<Typography variant="body2" color="text.secondary">
Tip: {selectedTemplate.template_type}
</Typography>
</Box>
)}
<TextField
fullWidth
margin="normal"
label="Test Mail Adresi"
type="email"
required
value={testMailAddress}
onChange={(e) => setTestMailAddress(e.target.value)}
helperText="Test mailinin gönderileceği adres"
/>
<TextField
fullWidth
margin="normal"
label="Şirket Adı (Placeholder)"
value={companyPlaceholder}
onChange={(e) => setCompanyPlaceholder(e.target.value)}
helperText="{{company_name}} yerine kullanılacak"
/>
<TextField
fullWidth
margin="normal"
label="Çalışan Adı (Placeholder)"
value={employeePlaceholder}
onChange={(e) => setEmployeePlaceholder(e.target.value)}
helperText="{{employee_name}} yerine kullanılacak"
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setTestMailDialogOpen(false)}>
İptal
</Button>
<Button
onClick={handleSendTestMail}
variant="contained"
startIcon={<SendIcon />}
>
Gönder
</Button>
</DialogActions>
</Dialog>
</Box>
);
}