diff --git a/frontend/src/components/EmailSettings.tsx b/frontend/src/components/EmailSettings.tsx new file mode 100644 index 000000000..02f8a4ba2 --- /dev/null +++ b/frontend/src/components/EmailSettings.tsx @@ -0,0 +1,364 @@ +import { useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { Mail, Send, Lock, Unlock, AlertTriangle, CheckCircle, Loader2 } from 'lucide-react'; +import { api } from '../api/client'; +import type { SMTPSettings, TestSMTPRequest } from '../api/client'; +import { Card, CardContent, CardHeader } from './Card'; +import { Button } from './Button'; +import { useToast } from '../contexts/ToastContext'; + +export function EmailSettings() { + const { t } = useTranslation(); + const { showToast } = useToast(); + const queryClient = useQueryClient(); + + const [smtpSettings, setSMTPSettings] = useState({ + smtp_host: '', + smtp_port: 587, + smtp_username: '', + smtp_password: '', + smtp_use_tls: true, + smtp_from_email: '', + smtp_from_name: 'BamBuddy', + }); + const [testEmail, setTestEmail] = useState(''); + + // Fetch SMTP settings + const { data: existingSettings, isLoading } = useQuery({ + queryKey: ['smtpSettings'], + queryFn: () => api.getSMTPSettings(), + }); + + // Fetch advanced auth status + const { data: advancedAuthStatus, refetch: refetchAuthStatus } = useQuery({ + queryKey: ['advancedAuthStatus'], + queryFn: () => api.getAdvancedAuthStatus(), + }); + + // Load existing settings when fetched + useState(() => { + if (existingSettings) { + setSMTPSettings({ + ...existingSettings, + smtp_password: '', // Never show password + }); + } + }); + + // Save SMTP settings + const saveMutation = useMutation({ + mutationFn: (settings: SMTPSettings) => api.saveSMTPSettings(settings), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['smtpSettings'] }); + queryClient.invalidateQueries({ queryKey: ['advancedAuthStatus'] }); + showToast('SMTP settings saved successfully', 'success'); + }, + onError: (error: Error) => { + showToast(error.message, 'error'); + }, + }); + + // Test SMTP connection + const testMutation = useMutation({ + mutationFn: (request: TestSMTPRequest) => api.testSMTP(request), + onSuccess: (data) => { + showToast(data.message, data.success ? 'success' : 'error'); + }, + onError: (error: Error) => { + showToast(error.message, 'error'); + }, + }); + + // Toggle advanced auth + const toggleAdvancedAuthMutation = useMutation({ + mutationFn: (enabled: boolean) => + enabled ? api.enableAdvancedAuth() : api.disableAdvancedAuth(), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['advancedAuthStatus'] }); + showToast(data.message, 'success'); + }, + onError: (error: Error) => { + showToast(error.message, 'error'); + }, + }); + + const handleSave = () => { + // Validate required fields + if (!smtpSettings.smtp_host || !smtpSettings.smtp_username || !smtpSettings.smtp_from_email) { + showToast('Please fill in all required fields', 'error'); + return; + } + saveMutation.mutate(smtpSettings); + }; + + const handleTest = () => { + if (!testEmail) { + showToast('Please enter a test email address', 'error'); + return; + } + if (!smtpSettings.smtp_host || !smtpSettings.smtp_username || !smtpSettings.smtp_password || !smtpSettings.smtp_from_email) { + showToast('Please fill in all SMTP settings before testing', 'error'); + return; + } + testMutation.mutate({ + smtp_host: smtpSettings.smtp_host, + smtp_port: smtpSettings.smtp_port, + smtp_username: smtpSettings.smtp_username, + smtp_password: smtpSettings.smtp_password, + smtp_use_tls: smtpSettings.smtp_use_tls, + smtp_from_email: smtpSettings.smtp_from_email, + test_recipient: testEmail, + }); + }; + + const handleToggleAdvancedAuth = () => { + if (!advancedAuthStatus?.advanced_auth_enabled && !advancedAuthStatus?.smtp_configured) { + showToast('Please configure and test SMTP settings first', 'error'); + return; + } + toggleAdvancedAuthMutation.mutate(!advancedAuthStatus?.advanced_auth_enabled); + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Advanced Authentication Toggle */} + + +
+
+ +

+ {t('settings.email.advancedAuth') || 'Advanced Authentication'} +

+
+ +
+
+ +
+ {advancedAuthStatus?.advanced_auth_enabled ? ( +
+
+ +
+

+ {t('settings.email.advancedAuthEnabled') || 'Advanced Authentication is enabled'} +

+
    +
  • {t('settings.email.feature1') || 'Passwords are auto-generated and emailed to new users'}
  • +
  • {t('settings.email.feature2') || 'Users can login with username or email'}
  • +
  • {t('settings.email.feature3') || 'Forgot password feature is available'}
  • +
  • {t('settings.email.feature4') || 'Admins can reset user passwords via email'}
  • +
+
+
+
+ ) : ( +
+
+ +
+

+ {t('settings.email.advancedAuthDisabled') || 'Advanced Authentication is disabled'} +

+

+ {t('settings.email.advancedAuthDisabledDesc') || 'Configure and test SMTP settings below, then enable advanced authentication to activate email-based features.'} +

+
+
+
+ )} +
+
+
+ + {/* SMTP Configuration */} + + +

+ {t('settings.email.smtpSettings') || 'SMTP Configuration'} +

+
+ +
+
+
+ + setSMTPSettings({ ...smtpSettings, smtp_host: e.target.value })} + placeholder="smtp.gmail.com" + className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" + /> +
+
+ + setSMTPSettings({ ...smtpSettings, smtp_port: parseInt(e.target.value) || 587 })} + className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" + /> +
+
+ +
+ + setSMTPSettings({ ...smtpSettings, smtp_username: e.target.value })} + placeholder="your.email@gmail.com" + className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" + /> +
+ +
+ + setSMTPSettings({ ...smtpSettings, smtp_password: e.target.value })} + placeholder={existingSettings ? '••••••••' : 'Enter password'} + className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" + /> +
+ +
+
+ + setSMTPSettings({ ...smtpSettings, smtp_from_email: e.target.value })} + placeholder="noreply@yourdomain.com" + className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" + /> +
+
+ + setSMTPSettings({ ...smtpSettings, smtp_from_name: e.target.value })} + placeholder="BamBuddy" + className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" + /> +
+
+ +
+ setSMTPSettings({ ...smtpSettings, smtp_use_tls: e.target.checked })} + className="w-4 h-4 rounded border-bambu-gray text-bambu-green focus:ring-bambu-green focus:ring-offset-0 bg-bambu-dark" + /> + +
+ +
+ +
+
+
+
+ + {/* Test SMTP */} + + +

+ {t('settings.email.testConnection') || 'Test SMTP Connection'} +

+
+ +
+
+ + setTestEmail(e.target.value)} + placeholder="test@example.com" + className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" + /> +
+ +
+
+
+
+ ); +} diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 6d7f7468b..3d6e14a33 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -1,5 +1,5 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock, ChevronDown, ChevronRight, Check, Save } from 'lucide-react'; +import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock, ChevronDown, ChevronRight, Check, Save, Mail } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { api } from '../api/client'; @@ -19,6 +19,7 @@ import { SpoolmanSettings } from '../components/SpoolmanSettings'; import { ExternalLinksSettings } from '../components/ExternalLinksSettings'; import { VirtualPrinterSettings } from '../components/VirtualPrinterSettings'; import { GitHubBackupSettings } from '../components/GitHubBackupSettings'; +import { EmailSettings } from '../components/EmailSettings'; import { APIBrowser } from '../components/APIBrowser'; import { virtualPrinterApi } from '../api/client'; import { defaultNavItems, getDefaultView, setDefaultView } from '../components/Layout'; @@ -28,7 +29,7 @@ import { useTheme, type ThemeStyle, type DarkBackground, type LightBackground, t import { useState, useEffect, useRef, useCallback } from 'react'; import { Palette } from 'lucide-react'; -const validTabs = ['general', 'network', 'plugs', 'notifications', 'filament', 'apikeys', 'virtual-printer', 'users', 'backup'] as const; +const validTabs = ['general', 'network', 'plugs', 'notifications', 'filament', 'apikeys', 'virtual-printer', 'users', 'email', 'backup'] as const; type TabType = typeof validTabs[number]; export function SettingsPage() { @@ -1025,6 +1026,17 @@ export function SettingsPage() { )} +