Add Mailgun admin tools and contact API
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useTranslations, useLocale } from 'next-intl'
|
||||
import { useAuth } from '@/hooks/use-auth'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -126,6 +126,7 @@ export default function BibleReaderNew() {
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
||||
const t = useTranslations('pages.bible')
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { user } = useAuth()
|
||||
|
||||
@@ -261,6 +262,68 @@ export default function BibleReaderNew() {
|
||||
})
|
||||
}, [locale, showAllVersions, selectedVersion])
|
||||
|
||||
// Handle URL parameters for bookmark navigation
|
||||
useEffect(() => {
|
||||
const urlVersion = searchParams.get('version')
|
||||
const urlBook = searchParams.get('book')
|
||||
const urlChapter = searchParams.get('chapter')
|
||||
const urlVerse = searchParams.get('verse')
|
||||
|
||||
if (urlVersion && versions.length > 0) {
|
||||
// Check if this version exists
|
||||
const version = versions.find(v => v.id === urlVersion)
|
||||
if (version && selectedVersion !== urlVersion) {
|
||||
setSelectedVersion(urlVersion)
|
||||
}
|
||||
}
|
||||
|
||||
if (urlBook && books.length > 0) {
|
||||
const book = books.find(b => b.id === urlBook)
|
||||
if (book && selectedBook !== urlBook) {
|
||||
setSelectedBook(urlBook)
|
||||
}
|
||||
}
|
||||
|
||||
if (urlChapter) {
|
||||
const chapter = parseInt(urlChapter)
|
||||
if (chapter && selectedChapter !== chapter) {
|
||||
setSelectedChapter(chapter)
|
||||
}
|
||||
}
|
||||
|
||||
if (urlVerse && verses.length > 0) {
|
||||
const verseNum = parseInt(urlVerse)
|
||||
if (verseNum) {
|
||||
// Highlight the verse
|
||||
setTimeout(() => {
|
||||
const verseElement = verseRefs.current[verseNum]
|
||||
if (verseElement) {
|
||||
verseElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
setHighlightedVerse(verseNum)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
}, [searchParams, versions, books, verses, selectedVersion, selectedBook, selectedChapter])
|
||||
|
||||
// Function to update URL without causing full page reload
|
||||
const updateUrl = useCallback((bookId?: string, chapter?: number, versionId?: string) => {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (versionId || selectedVersion) {
|
||||
params.set('version', versionId || selectedVersion)
|
||||
}
|
||||
if (bookId || selectedBook) {
|
||||
params.set('book', bookId || selectedBook)
|
||||
}
|
||||
if (chapter || selectedChapter) {
|
||||
params.set('chapter', String(chapter || selectedChapter))
|
||||
}
|
||||
|
||||
const newUrl = `/${locale}/bible?${params.toString()}`
|
||||
router.replace(newUrl, { scroll: false })
|
||||
}, [locale, selectedVersion, selectedBook, selectedChapter, router])
|
||||
|
||||
// Fetch books when version changes
|
||||
useEffect(() => {
|
||||
if (selectedVersion) {
|
||||
@@ -431,16 +494,6 @@ export default function BibleReaderNew() {
|
||||
const currentBook = books.find(book => book.id === selectedBook)
|
||||
const maxChapters = currentBook?.chapters?.length || 1
|
||||
|
||||
const updateUrl = (bookId: string, chapter: number, version?: string) => {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set('book', bookId)
|
||||
url.searchParams.set('chapter', chapter.toString())
|
||||
if (version) {
|
||||
url.searchParams.set('version', version)
|
||||
}
|
||||
window.history.replaceState({}, '', url.toString())
|
||||
}
|
||||
|
||||
const scrollToVerse = (verseNum: number) => {
|
||||
const verseElement = verseRefs.current[verseNum]
|
||||
if (verseElement) {
|
||||
|
||||
@@ -50,6 +50,7 @@ interface BookmarkItem {
|
||||
bookId: string
|
||||
chapterNum: number
|
||||
verseNum?: number
|
||||
versionId: string
|
||||
}
|
||||
verse?: {
|
||||
id: string
|
||||
@@ -118,7 +119,8 @@ export default function BookmarksPage() {
|
||||
const handleNavigateToBookmark = (bookmark: BookmarkItem) => {
|
||||
const params = new URLSearchParams({
|
||||
book: bookmark.navigation.bookId,
|
||||
chapter: bookmark.navigation.chapterNum.toString()
|
||||
chapter: bookmark.navigation.chapterNum.toString(),
|
||||
version: bookmark.navigation.versionId
|
||||
})
|
||||
|
||||
if (bookmark.navigation.verseNum) {
|
||||
|
||||
@@ -50,20 +50,30 @@ export default function Contact() {
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
// Simulate form submission
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Here you would typically send the data to your API
|
||||
console.log('Form submitted:', formData)
|
||||
|
||||
setFormData({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
setShowSuccess(true)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (response.ok && data.success) {
|
||||
setFormData({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
})
|
||||
setShowSuccess(true)
|
||||
} else {
|
||||
console.error('Contact form error:', data.error)
|
||||
setShowError(true)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Contact form submission error:', error)
|
||||
setShowError(true)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
|
||||
437
app/admin/mailgun/page.tsx
Normal file
437
app/admin/mailgun/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
231
app/api/admin/mailgun/route.ts
Normal file
231
app/api/admin/mailgun/route.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { verifyAdminAuth } from '@/lib/admin-auth'
|
||||
import { mailgunService } from '@/lib/mailgun'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const adminUser = await verifyAdminAuth(request)
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const settings = await prisma.mailgunSettings.findFirst({
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
domain: true,
|
||||
region: true,
|
||||
fromEmail: true,
|
||||
fromName: true,
|
||||
replyToEmail: true,
|
||||
isEnabled: true,
|
||||
testMode: true,
|
||||
webhookUrl: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
// Don't return the API key for security
|
||||
apiKey: false
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: settings
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching Mailgun settings:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const adminUser = await verifyAdminAuth(request)
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const {
|
||||
apiKey,
|
||||
domain,
|
||||
region = 'US',
|
||||
fromEmail,
|
||||
fromName,
|
||||
replyToEmail,
|
||||
isEnabled = false,
|
||||
testMode = true,
|
||||
webhookUrl
|
||||
} = body
|
||||
|
||||
// Validate required fields
|
||||
if (!apiKey || !domain || !fromEmail || !fromName) {
|
||||
return NextResponse.json({
|
||||
error: 'API key, domain, from email, and from name are required'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(fromEmail)) {
|
||||
return NextResponse.json({
|
||||
error: 'Invalid from email format'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
if (replyToEmail && !emailRegex.test(replyToEmail)) {
|
||||
return NextResponse.json({
|
||||
error: 'Invalid reply-to email format'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// Disable existing settings
|
||||
await prisma.mailgunSettings.updateMany({
|
||||
data: { isEnabled: false }
|
||||
})
|
||||
|
||||
// Create new settings
|
||||
const settings = await prisma.mailgunSettings.create({
|
||||
data: {
|
||||
apiKey,
|
||||
domain,
|
||||
region,
|
||||
fromEmail,
|
||||
fromName,
|
||||
replyToEmail,
|
||||
isEnabled,
|
||||
testMode,
|
||||
webhookUrl,
|
||||
updatedBy: adminUser.id
|
||||
}
|
||||
})
|
||||
|
||||
// Clear service cache
|
||||
mailgunService.clearCache()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: settings.id,
|
||||
domain: settings.domain,
|
||||
region: settings.region,
|
||||
fromEmail: settings.fromEmail,
|
||||
fromName: settings.fromName,
|
||||
replyToEmail: settings.replyToEmail,
|
||||
isEnabled: settings.isEnabled,
|
||||
testMode: settings.testMode,
|
||||
webhookUrl: settings.webhookUrl,
|
||||
createdAt: settings.createdAt,
|
||||
updatedAt: settings.updatedAt
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error creating Mailgun settings:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const adminUser = await verifyAdminAuth(request)
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const {
|
||||
id,
|
||||
apiKey,
|
||||
domain,
|
||||
region,
|
||||
fromEmail,
|
||||
fromName,
|
||||
replyToEmail,
|
||||
isEnabled,
|
||||
testMode,
|
||||
webhookUrl
|
||||
} = body
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'Settings ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!domain || !fromEmail || !fromName) {
|
||||
return NextResponse.json({
|
||||
error: 'Domain, from email, and from name are required'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(fromEmail)) {
|
||||
return NextResponse.json({
|
||||
error: 'Invalid from email format'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
if (replyToEmail && !emailRegex.test(replyToEmail)) {
|
||||
return NextResponse.json({
|
||||
error: 'Invalid reply-to email format'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// If enabling this setting, disable others
|
||||
if (isEnabled) {
|
||||
await prisma.mailgunSettings.updateMany({
|
||||
where: { id: { not: id } },
|
||||
data: { isEnabled: false }
|
||||
})
|
||||
}
|
||||
|
||||
// Build update data
|
||||
const updateData: any = {
|
||||
domain,
|
||||
region,
|
||||
fromEmail,
|
||||
fromName,
|
||||
replyToEmail,
|
||||
isEnabled,
|
||||
testMode,
|
||||
webhookUrl,
|
||||
updatedBy: adminUser.id
|
||||
}
|
||||
|
||||
// Only update API key if provided
|
||||
if (apiKey) {
|
||||
updateData.apiKey = apiKey
|
||||
}
|
||||
|
||||
const settings = await prisma.mailgunSettings.update({
|
||||
where: { id },
|
||||
data: updateData
|
||||
})
|
||||
|
||||
// Clear service cache
|
||||
mailgunService.clearCache()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: settings.id,
|
||||
domain: settings.domain,
|
||||
region: settings.region,
|
||||
fromEmail: settings.fromEmail,
|
||||
fromName: settings.fromName,
|
||||
replyToEmail: settings.replyToEmail,
|
||||
isEnabled: settings.isEnabled,
|
||||
testMode: settings.testMode,
|
||||
webhookUrl: settings.webhookUrl,
|
||||
createdAt: settings.createdAt,
|
||||
updatedAt: settings.updatedAt
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error updating Mailgun settings:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
100
app/api/admin/mailgun/test/route.ts
Normal file
100
app/api/admin/mailgun/test/route.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { verifyAdminAuth } from '@/lib/admin-auth'
|
||||
import { mailgunService } from '@/lib/mailgun'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const adminUser = await verifyAdminAuth(request)
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { testType, email } = body
|
||||
|
||||
if (testType === 'connection') {
|
||||
// Test connection to Mailgun
|
||||
const result = await mailgunService.testConnection()
|
||||
return NextResponse.json(result)
|
||||
}
|
||||
|
||||
if (testType === 'email') {
|
||||
// Send test email
|
||||
if (!email) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Email address is required for email test'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const result = await mailgunService.sendEmail({
|
||||
to: email,
|
||||
subject: 'Biblical Guide - Email Configuration Test',
|
||||
text: `Hello,
|
||||
|
||||
This is a configuration test email from Biblical Guide.
|
||||
|
||||
Your email system has been successfully configured and is working properly. You can now receive important notifications from our platform.
|
||||
|
||||
Best regards,
|
||||
The Biblical Guide Team
|
||||
|
||||
---
|
||||
This is an automated message. Please do not reply to this email.
|
||||
Time: ${new Date().toLocaleString()}`,
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Biblical Guide - Email Configuration Test</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
|
||||
<h2 style="color: #2c3e50; margin: 0;">Biblical Guide</h2>
|
||||
<p style="margin: 5px 0 0 0; color: #6c757d;">Email Configuration Test</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: white; padding: 20px; border-radius: 8px; border: 1px solid #e9ecef;">
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>This is a configuration test email from Biblical Guide.</p>
|
||||
|
||||
<p><strong>Your email system has been successfully configured and is working properly.</strong> You can now receive important notifications from our platform.</p>
|
||||
|
||||
<div style="background-color: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; padding: 15px; margin: 20px 0;">
|
||||
<p style="margin: 0; color: #155724;"><strong>✓ Configuration successful</strong></p>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The Biblical Guide Team</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; font-size: 12px; color: #6c757d;">
|
||||
<p style="margin: 0;">This is an automated message. Please do not reply to this email.</p>
|
||||
<p style="margin: 5px 0 0 0;">Time: ${new Date().toLocaleString()}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
})
|
||||
|
||||
return NextResponse.json(result)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Invalid test type'
|
||||
}, { status: 400 })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error testing Mailgun:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Test failed'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -86,12 +86,13 @@ export async function GET(request: Request) {
|
||||
id: bookmark.id,
|
||||
type: 'chapter' as const,
|
||||
title: `${bookmark.book.name} ${bookmark.chapterNum}`,
|
||||
subtitle: bookmark.book.version.name,
|
||||
subtitle: `${bookmark.book.version.name} - ${bookmark.book.version.abbreviation}`,
|
||||
note: bookmark.note,
|
||||
createdAt: bookmark.createdAt,
|
||||
navigation: {
|
||||
bookId: bookmark.bookId,
|
||||
chapterNum: bookmark.chapterNum
|
||||
chapterNum: bookmark.chapterNum,
|
||||
versionId: bookmark.book.versionId
|
||||
},
|
||||
book: bookmark.book
|
||||
}))
|
||||
@@ -100,7 +101,7 @@ export async function GET(request: Request) {
|
||||
id: bookmark.id,
|
||||
type: 'verse' as const,
|
||||
title: `${bookmark.verse.chapter.book.name} ${bookmark.verse.chapter.chapterNum}:${bookmark.verse.verseNum}`,
|
||||
subtitle: bookmark.verse.chapter.book.version.name,
|
||||
subtitle: `${bookmark.verse.chapter.book.version.name} - ${bookmark.verse.chapter.book.version.abbreviation}`,
|
||||
note: bookmark.note,
|
||||
createdAt: bookmark.createdAt,
|
||||
color: bookmark.color,
|
||||
@@ -108,7 +109,8 @@ export async function GET(request: Request) {
|
||||
navigation: {
|
||||
bookId: bookmark.verse.chapter.bookId,
|
||||
chapterNum: bookmark.verse.chapter.chapterNum,
|
||||
verseNum: bookmark.verse.verseNum
|
||||
verseNum: bookmark.verse.verseNum,
|
||||
versionId: bookmark.verse.chapter.book.versionId
|
||||
},
|
||||
verse: bookmark.verse
|
||||
}))
|
||||
|
||||
77
app/api/contact/route.ts
Normal file
77
app/api/contact/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { mailgunService } from '@/lib/mailgun'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
const contactSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(100),
|
||||
email: z.string().email('Invalid email address'),
|
||||
subject: z.string().min(1, 'Subject is required').max(200),
|
||||
message: z.string().min(10, 'Message must be at least 10 characters').max(5000)
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
// Validate input
|
||||
const validationResult = contactSchema.safeParse(body)
|
||||
if (!validationResult.success) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Invalid form data',
|
||||
details: validationResult.error.errors
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const { name, email, subject, message } = validationResult.data
|
||||
|
||||
// Basic spam prevention - check for common spam indicators
|
||||
const spamIndicators = [
|
||||
message.includes('http://'),
|
||||
message.includes('https://'),
|
||||
message.includes('www.'),
|
||||
message.includes('bitcoin'),
|
||||
message.includes('cryptocurrency'),
|
||||
message.length < 10,
|
||||
name.length < 2
|
||||
]
|
||||
|
||||
const spamScore = spamIndicators.filter(Boolean).length
|
||||
if (spamScore >= 2) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Message flagged as potential spam'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// Send email using Mailgun
|
||||
const emailResult = await mailgunService.sendContactForm({
|
||||
name,
|
||||
email,
|
||||
subject,
|
||||
message
|
||||
})
|
||||
|
||||
if (emailResult.success) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Your message has been sent successfully!'
|
||||
})
|
||||
} else {
|
||||
console.error('Contact form email failed:', emailResult.error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Failed to send message. Please try again later.'
|
||||
}, { status: 500 })
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Contact form error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user