Files
balikci/frontend/src/pages/Settings.jsx

635 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import {
Box,
Paper,
Typography,
TextField,
Button,
Alert,
CircularProgress,
Divider,
FormControlLabel,
Checkbox,
Grid,
} from '@mui/material';
import { Save, Send } from '@mui/icons-material';
import api from '../services/api';
function Settings() {
const [settings, setSettings] = useState({
base_url: '',
frontend_url: '',
cors_enabled: false,
gmail_user: '',
gmail_app_password: '',
gmail_from_name: '',
telegram_bot_token: '',
telegram_chat_id: '',
ollama_server_url: '',
ollama_model: '',
});
const [adminInfo, setAdminInfo] = useState({
username: '',
current_password: '',
new_password: '',
confirm_password: '',
});
const [loading, setLoading] = useState(true);
const [testLoading, setTestLoading] = useState({ mail: false, telegram: false, ollama: false });
const [alerts, setAlerts] = useState({ mail: null, telegram: null, ollama: null, admin: null });
const [ollamaModels, setOllamaModels] = useState([]);
const [adminSaving, setAdminSaving] = useState(false);
useEffect(() => {
loadSettings();
loadAdminInfo();
}, []);
const loadSettings = async () => {
try {
const response = await api.get('/api/settings');
const data = response.data.data || {};
// Convert array to object
const settingsObj = {};
if (Array.isArray(data)) {
data.forEach(item => {
settingsObj[item.key] = item.value === '********' ? '' : item.value;
});
}
setSettings({
base_url: settingsObj.base_url || '',
frontend_url: settingsObj.frontend_url || '',
cors_enabled: settingsObj.cors_enabled === 'true',
gmail_user: settingsObj.gmail_user || '',
gmail_app_password: settingsObj.gmail_password || '',
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);
} finally {
setLoading(false);
}
};
const loadAdminInfo = async () => {
try {
const response = await api.get('/api/settings/admin');
if (response.data.success && response.data.data) {
setAdminInfo({
username: response.data.data.username || '',
current_password: '',
new_password: '',
confirm_password: '',
});
}
} catch (error) {
console.error('Failed to load admin info:', error);
}
};
const handleSave = async () => {
try {
await Promise.all([
api.put('/api/settings/system', {
base_url: settings.base_url,
frontend_url: settings.frontend_url,
cors_enabled: settings.cors_enabled,
}, { withCredentials: true }),
api.put('/api/settings/gmail', {
gmail_user: settings.gmail_user,
gmail_app_password: settings.gmail_app_password,
gmail_from_name: settings.gmail_from_name,
}, { withCredentials: true }),
api.put('/api/settings/telegram', {
telegram_bot_token: settings.telegram_bot_token,
telegram_chat_id: settings.telegram_chat_id,
}, { withCredentials: true }),
api.put('/api/ollama/settings', {
ollama_server_url: settings.ollama_server_url,
ollama_model: settings.ollama_model,
}, { withCredentials: true }),
]);
alert('Ayarlar kaydedildi!');
} catch (error) {
console.error('Failed to save settings:', error);
alert('Ayarlar kaydedilemedi: ' + (error.response?.data?.error || error.message));
}
};
const handleTestMail = async () => {
setTestLoading({ ...testLoading, mail: true });
try {
const response = await api.post('/api/settings/test-gmail', {});
setAlerts({ ...alerts, mail: { severity: 'success', message: response.data.message } });
} catch (error) {
setAlerts({
...alerts,
mail: { severity: 'error', message: error.response?.data?.error || 'Test başarısız' },
});
} finally {
setTestLoading({ ...testLoading, mail: false });
}
};
const handleTestTelegram = async () => {
setTestLoading({ ...testLoading, telegram: true });
try {
const response = await api.post('/api/settings/test-telegram', {});
setAlerts({ ...alerts, telegram: { severity: 'success', message: response.data.message } });
} catch (error) {
setAlerts({
...alerts,
telegram: {
severity: 'error',
message: error.response?.data?.error || 'Test başarısız',
},
});
} finally {
setTestLoading({ ...testLoading, telegram: false });
}
};
const handleTestOllama = async () => {
setTestLoading({ ...testLoading, ollama: true });
setAlerts({ ...alerts, ollama: null });
try {
// First save the settings if they are filled
if (settings.ollama_server_url && settings.ollama_model) {
await axios.put(`${API_URL}/api/ollama/settings`, {
ollama_server_url: settings.ollama_server_url,
ollama_model: settings.ollama_model,
}, { withCredentials: true });
}
const response = await api.get('/api/ollama/test');
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 });
}
};
const handleSaveAdmin = async () => {
setAdminSaving(true);
setAlerts({ ...alerts, admin: null });
try {
const updateData = {};
// Only include fields that are being changed
if (adminInfo.username && adminInfo.username.trim() !== '') {
updateData.username = adminInfo.username.trim();
}
if (adminInfo.new_password) {
updateData.new_password = adminInfo.new_password;
updateData.confirm_password = adminInfo.confirm_password;
updateData.current_password = adminInfo.current_password;
}
const response = await api.put('/api/settings/admin', updateData);
if (response.data.success) {
setAlerts({
...alerts,
admin: { severity: 'success', message: response.data.message || 'Admin bilgileri güncellendi' },
});
// Clear password fields
setAdminInfo({
...adminInfo,
current_password: '',
new_password: '',
confirm_password: '',
});
// Reload admin info to get updated username
await loadAdminInfo();
}
} catch (error) {
setAlerts({
...alerts,
admin: {
severity: 'error',
message: error.response?.data?.error || 'Admin bilgileri güncellenemedi',
},
});
} finally {
setAdminSaving(false);
}
};
if (loading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
<CircularProgress />
</Box>
);
}
return (
<Box>
<Typography variant="h4" gutterBottom>
Sistem Ayarları
</Typography>
<Grid container spacing={3}>
{/* System Settings */}
<Grid size={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
🌐 Genel Ayarlar
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
Domain ve genel sistem ayarları
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
<strong>Tek Domain (Önerilen):</strong> Frontend ve backend aynı domainde, path ile ayrılır<br/>
<strong>İki Domain:</strong> Frontend ve backend farklı domainlerde (CORS gerekir)
</Typography>
<FormControlLabel
control={
<Checkbox
checked={settings.cors_enabled}
onChange={(e) =>
setSettings({ ...settings, cors_enabled: e.target.checked })
}
/>
}
label="İki Ayrı Domain Kullan (CORS Aktif Et)"
/>
<TextField
fullWidth
margin="normal"
label={settings.cors_enabled ? "Backend Domain (Base URL)" : "Domain (Base URL)"}
type="url"
placeholder="https://yourdomain.com"
value={settings.base_url}
onChange={(e) =>
setSettings({ ...settings, base_url: e.target.value })
}
helperText={
settings.cors_enabled
? "Backend API domain'i. Örnek: https://api.yourdomain.com"
: "Hem frontend hem backend için kullanılacak domain. Örnek: https://yourdomain.com"
}
/>
{settings.cors_enabled && (
<TextField
fullWidth
margin="normal"
label="Frontend Domain"
type="url"
placeholder="https://panel.yourdomain.com"
value={settings.frontend_url}
onChange={(e) =>
setSettings({ ...settings, frontend_url: e.target.value })
}
helperText="Frontend panel domain'i. CORS için gerekli."
/>
)}
</Paper>
</Grid>
{/* Gmail Settings */}
<Grid size={{ xs: 12, md: 6 }}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
📧 Gmail Ayarları
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
Gmail App Password kullanın (2FA aktif olmalı)
</Typography>
<TextField
fullWidth
margin="normal"
label="Gmail Adresi"
type="email"
value={settings.gmail_user}
onChange={(e) =>
setSettings({ ...settings, gmail_user: e.target.value })
}
/>
<TextField
fullWidth
margin="normal"
label="App Password"
type="password"
value={settings.gmail_app_password}
onChange={(e) =>
setSettings({ ...settings, gmail_app_password: e.target.value })
}
helperText="Google Hesap → Güvenlik → 2FA → Uygulama Şifreleri"
/>
<TextField
fullWidth
margin="normal"
label="Gönderen Adı (Varsayılan)"
value={settings.gmail_from_name}
onChange={(e) =>
setSettings({ ...settings, gmail_from_name: e.target.value })
}
placeholder="Güvenlik Ekibi"
helperText="Mail gönderirken görünecek varsayılan isim"
/>
{alerts.mail && (
<Alert severity={alerts.mail.severity} sx={{ mt: 2 }}>
{alerts.mail.message}
</Alert>
)}
<Box mt={2} display="flex" gap={2}>
<Button
variant="contained"
startIcon={<Save />}
onClick={handleSave}
>
Kaydet
</Button>
<Button
variant="outlined"
startIcon={<Send />}
onClick={handleTestMail}
disabled={testLoading.mail}
>
Test Mail Gönder
</Button>
</Box>
</Paper>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
Telegram Ayarları
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
@BotFather'dan bot token alın, @userinfobot'dan chat ID öğrenin
</Typography>
<TextField
fullWidth
margin="normal"
label="Bot Token"
type="password"
value={settings.telegram_bot_token}
onChange={(e) =>
setSettings({ ...settings, telegram_bot_token: e.target.value })
}
/>
<TextField
fullWidth
margin="normal"
label="Chat ID"
value={settings.telegram_chat_id}
onChange={(e) =>
setSettings({ ...settings, telegram_chat_id: e.target.value })
}
/>
{alerts.telegram && (
<Alert severity={alerts.telegram.severity} sx={{ mt: 2 }}>
{alerts.telegram.message}
</Alert>
)}
<Box mt={2} display="flex" gap={2}>
<Button
variant="contained"
startIcon={<Save />}
onClick={handleSave}
>
Kaydet
</Button>
<Button
variant="outlined"
startIcon={<Send />}
onClick={handleTestTelegram}
disabled={testLoading.telegram}
>
Test Bildirimi
</Button>
</Box>
</Paper>
</Grid>
{/* Admin User Settings */}
<Grid size={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
🔐 Admin Kullanıcı Bilgileri
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
Kullanıcı adı ve şifrenizi değiştirebilirsiniz
</Typography>
<TextField
fullWidth
margin="normal"
label="Kullanıcı Adı"
value={adminInfo.username}
onChange={(e) =>
setAdminInfo({ ...adminInfo, username: e.target.value })
}
helperText="En az 3 karakter olmalıdır"
/>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle2" gutterBottom>
Şifre Değiştir (Opsiyonel)
</Typography>
<TextField
fullWidth
margin="normal"
label="Mevcut Şifre"
type="password"
value={adminInfo.current_password}
onChange={(e) =>
setAdminInfo({ ...adminInfo, current_password: e.target.value })
}
helperText="Yeni şifre belirlemek için mevcut şifrenizi girin"
/>
<TextField
fullWidth
margin="normal"
label="Yeni Şifre"
type="password"
value={adminInfo.new_password}
onChange={(e) =>
setAdminInfo({ ...adminInfo, new_password: e.target.value })
}
helperText="En az 8 karakter olmalıdır"
/>
<TextField
fullWidth
margin="normal"
label="Yeni Şifre (Tekrar)"
type="password"
value={adminInfo.confirm_password}
onChange={(e) =>
setAdminInfo({ ...adminInfo, confirm_password: e.target.value })
}
helperText="Yeni şifreyi tekrar girin"
/>
{alerts.admin && (
<Alert severity={alerts.admin.severity} sx={{ mt: 2 }}>
{alerts.admin.message}
</Alert>
)}
<Box mt={2}>
<Button
variant="contained"
startIcon={adminSaving ? <CircularProgress size={20} /> : <Save />}
onClick={handleSaveAdmin}
disabled={adminSaving}
>
{adminSaving ? 'Kaydediliyor...' : 'Admin Bilgilerini Kaydet'}
</Button>
</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 (tıklayarak seçin):
</Typography>
{ollamaModels.map((model, idx) => (
<Typography
key={idx}
variant="body2"
sx={{
cursor: 'pointer',
'&:hover': { backgroundColor: 'action.hover' },
p: 0.5,
borderRadius: 1,
}}
onClick={() => setSettings({ ...settings, ollama_model: model.name })}
>
{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 }}>
<Typography variant="h6" gutterBottom>
Tracking URL Bilgisi
</Typography>
<Divider sx={{ my: 2 }} />
<Typography variant="body2" color="text.secondary">
Tracking URL formatı: <strong>http://your-domain.com/t/TOKEN</strong>
</Typography>
<Typography variant="body2" color="text.secondary" mt={1}>
Bu URL'ler mail şablonlarında otomatik olarak oluşturulur ve gönderilir.
</Typography>
</Paper>
</Box>
);
}
export default Settings;