Add Mailgun admin tools and contact API

This commit is contained in:
2025-09-24 13:59:26 +00:00
parent 6329ad0618
commit 1054f5d817
13 changed files with 1459 additions and 29 deletions

437
app/admin/mailgun/page.tsx Normal file
View File

@@ -0,0 +1,437 @@
'use client'
import { useState, useEffect } from 'react'
import {
Box,
Paper,
Typography,
TextField,
Button,
FormControl,
InputLabel,
Select,
MenuItem,
Switch,
FormControlLabel,
Alert,
Divider,
Card,
CardContent,
InputAdornment,
IconButton,
Chip
} from '@mui/material'
import {
Save as SaveIcon,
PlayArrow as TestIcon,
Visibility,
VisibilityOff,
Email as EmailIcon,
Settings as SettingsIcon,
CheckCircle,
Error as ErrorIcon
} from '@mui/icons-material'
interface MailgunSettings {
id?: string
apiKey?: string
domain: string
region: string
fromEmail: string
fromName: string
replyToEmail?: string
isEnabled: boolean
testMode: boolean
webhookUrl?: string
createdAt?: string
updatedAt?: string
}
export default function MailgunSettingsPage() {
const [settings, setSettings] = useState<MailgunSettings>({
domain: '',
region: 'US',
fromEmail: '',
fromName: '',
isEnabled: false,
testMode: true
})
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [testing, setTesting] = useState(false)
const [showApiKey, setShowApiKey] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const [testEmail, setTestEmail] = useState('')
const [connectionStatus, setConnectionStatus] = useState<'unknown' | 'connected' | 'error'>('unknown')
useEffect(() => {
loadSettings()
}, [])
const loadSettings = async () => {
setLoading(true)
try {
const response = await fetch('/api/admin/mailgun', {
credentials: 'include'
})
if (response.ok) {
const data = await response.json()
if (data.success && data.data) {
setSettings(data.data)
if (data.data.isEnabled) {
testConnection()
}
}
}
} catch (error) {
console.error('Error loading settings:', error)
setMessage({ type: 'error', text: 'Failed to load settings' })
} finally {
setLoading(false)
}
}
const handleSave = async () => {
setSaving(true)
setMessage(null)
try {
const method = settings.id ? 'PUT' : 'POST'
const response = await fetch('/api/admin/mailgun', {
method,
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(settings)
})
const data = await response.json()
if (response.ok && data.success) {
setSettings(data.data)
setMessage({ type: 'success', text: 'Settings saved successfully' })
if (data.data.isEnabled) {
testConnection()
}
} else {
setMessage({ type: 'error', text: data.error || 'Failed to save settings' })
}
} catch (error) {
console.error('Error saving settings:', error)
setMessage({ type: 'error', text: 'Failed to save settings' })
} finally {
setSaving(false)
}
}
const testConnection = async () => {
setTesting(true)
try {
const response = await fetch('/api/admin/mailgun/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({ testType: 'connection' })
})
const data = await response.json()
if (data.success) {
setConnectionStatus('connected')
setMessage({ type: 'success', text: 'Connection test successful' })
} else {
setConnectionStatus('error')
setMessage({ type: 'error', text: `Connection failed: ${data.error}` })
}
} catch (error) {
setConnectionStatus('error')
setMessage({ type: 'error', text: 'Connection test failed' })
} finally {
setTesting(false)
}
}
const sendTestEmail = async () => {
if (!testEmail) {
setMessage({ type: 'error', text: 'Please enter a test email address' })
return
}
setTesting(true)
try {
const response = await fetch('/api/admin/mailgun/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({ testType: 'email', email: testEmail })
})
const data = await response.json()
if (data.success) {
setMessage({ type: 'success', text: `Test email sent successfully! ${data.messageId ? `Message ID: ${data.messageId}` : ''}` })
} else {
setMessage({ type: 'error', text: `Failed to send test email: ${data.error}` })
}
} catch (error) {
setMessage({ type: 'error', text: 'Failed to send test email' })
} finally {
setTesting(false)
}
}
const handleChange = (field: keyof MailgunSettings, value: any) => {
setSettings(prev => ({ ...prev, [field]: value }))
}
return (
<Box sx={{ maxWidth: 1200, mx: 'auto', p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<EmailIcon sx={{ mr: 2, color: 'primary.main' }} />
<Typography variant="h4" component="h1">
Mailgun Settings
</Typography>
</Box>
{message && (
<Alert
severity={message.type}
sx={{ mb: 3 }}
onClose={() => setMessage(null)}
>
{message.text}
</Alert>
)}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Connection Status */}
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<SettingsIcon sx={{ mr: 2 }} />
<Typography variant="h6">Connection Status</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{connectionStatus === 'connected' && (
<Chip
label="Connected"
color="success"
icon={<CheckCircle />}
size="small"
/>
)}
{connectionStatus === 'error' && (
<Chip
label="Connection Error"
color="error"
icon={<ErrorIcon />}
size="small"
/>
)}
{connectionStatus === 'unknown' && (
<Chip
label="Unknown"
color="default"
size="small"
/>
)}
<Button
variant="outlined"
size="small"
onClick={testConnection}
disabled={testing || !settings.isEnabled}
startIcon={<TestIcon />}
>
Test Connection
</Button>
</Box>
</Box>
</CardContent>
</Card>
<Box sx={{ display: 'flex', gap: 3, flexDirection: { xs: 'column', md: 'row' } }}>
{/* Main Settings */}
<Paper sx={{ p: 3, flex: { xs: '1 1 100%', md: '2 1 0' } }}>
<Typography variant="h6" sx={{ mb: 3 }}>
Mailgun Configuration
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<TextField
label="API Key"
type={showApiKey ? 'text' : 'password'}
value={settings.apiKey || ''}
onChange={(e) => handleChange('apiKey', e.target.value)}
placeholder="key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowApiKey(!showApiKey)}
edge="end"
>
{showApiKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
helperText="Your Mailgun API key"
/>
<TextField
label="Domain"
value={settings.domain}
onChange={(e) => handleChange('domain', e.target.value)}
placeholder="mg.yourdomain.com"
fullWidth
helperText="Your Mailgun domain"
/>
<FormControl fullWidth>
<InputLabel>Region</InputLabel>
<Select
value={settings.region}
onChange={(e) => handleChange('region', e.target.value)}
label="Region"
>
<MenuItem value="US">US</MenuItem>
<MenuItem value="EU">EU</MenuItem>
</Select>
</FormControl>
<Divider />
<Typography variant="h6">Email Settings</Typography>
<TextField
label="From Email"
type="email"
value={settings.fromEmail}
onChange={(e) => handleChange('fromEmail', e.target.value)}
placeholder="noreply@yourdomain.com"
fullWidth
helperText="Default sender email address"
/>
<TextField
label="From Name"
value={settings.fromName}
onChange={(e) => handleChange('fromName', e.target.value)}
placeholder="Biblical Guide"
fullWidth
helperText="Default sender name"
/>
<TextField
label="Reply-To Email (Optional)"
type="email"
value={settings.replyToEmail || ''}
onChange={(e) => handleChange('replyToEmail', e.target.value)}
placeholder="support@yourdomain.com"
fullWidth
helperText="Default reply-to address"
/>
<TextField
label="Webhook URL (Optional)"
type="url"
value={settings.webhookUrl || ''}
onChange={(e) => handleChange('webhookUrl', e.target.value)}
placeholder="https://yourdomain.com/api/webhooks/mailgun"
fullWidth
helperText="URL for Mailgun webhooks (tracking, bounces, etc.)"
/>
<Divider />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.isEnabled}
onChange={(e) => handleChange('isEnabled', e.target.checked)}
/>
}
label="Enable Mailgun"
/>
<FormControlLabel
control={
<Switch
checked={settings.testMode}
onChange={(e) => handleChange('testMode', e.target.checked)}
/>
}
label="Test Mode (emails won't actually be sent)"
/>
</Box>
<Button
variant="contained"
onClick={handleSave}
disabled={saving || loading}
startIcon={<SaveIcon />}
size="large"
sx={{ alignSelf: 'flex-start' }}
>
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</Box>
</Paper>
{/* Test Email */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, flex: { xs: '1 1 100%', md: '1 1 0' } }}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" sx={{ mb: 3 }}>
Test Email
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Test Email Address"
type="email"
value={testEmail}
onChange={(e) => setTestEmail(e.target.value)}
placeholder="test@example.com"
fullWidth
/>
<Button
variant="outlined"
onClick={sendTestEmail}
disabled={testing || !settings.isEnabled || !testEmail}
startIcon={<EmailIcon />}
fullWidth
>
{testing ? 'Sending...' : 'Send Test Email'}
</Button>
</Box>
{settings.testMode && (
<Alert severity="info" sx={{ mt: 2 }}>
Test mode is enabled. Emails will be logged but not actually sent.
</Alert>
)}
</Paper>
{settings.updatedAt && (
<Paper sx={{ p: 2 }}>
<Typography variant="body2" color="text.secondary">
Last updated: {new Date(settings.updatedAt).toLocaleString()}
</Typography>
</Paper>
)}
</Box>
</Box>
</Box>
</Box>
)
}