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

View 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 })
}
}

View 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 })
}
}

View File

@@ -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
View 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 })
}
}