Add Mailgun admin tools and contact API
This commit is contained in:
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