130 lines
4.1 KiB
TypeScript
130 lines
4.1 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import nodemailer from 'nodemailer'
|
|
import { createApproveToken } from '@/lib/token'
|
|
|
|
interface ChannelSelection {
|
|
id: string
|
|
name: string
|
|
country: string
|
|
categories: string[]
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json()
|
|
const { email, channels } = body as { email: string; channels: ChannelSelection[] }
|
|
|
|
// Validate email
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
if (!email || !emailRegex.test(email)) {
|
|
return NextResponse.json(
|
|
{ error: 'A valid email is required' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
// Validate channels
|
|
if (!Array.isArray(channels) || channels.length === 0 || channels.length > 20) {
|
|
return NextResponse.json(
|
|
{ error: 'Select between 1 and 20 channels' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
for (const ch of channels) {
|
|
if (!ch.id || !ch.name) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid channel data' },
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
}
|
|
|
|
const smtpHost = process.env.SMTP_HOST
|
|
const smtpUser = process.env.SMTP_USER
|
|
const smtpPass = process.env.SMTP_PASS
|
|
if (!smtpHost || !smtpUser || !smtpPass) {
|
|
console.error('SMTP credentials not configured')
|
|
return NextResponse.json(
|
|
{ error: 'Email service not configured' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
|
|
const adminEmail = process.env.ADMIN_EMAIL || 'jeff@jeffemmett.com'
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
host: smtpHost,
|
|
port: Number(process.env.SMTP_PORT) || 587,
|
|
secure: false,
|
|
auth: { user: smtpUser, pass: smtpPass },
|
|
tls: { rejectUnauthorized: false },
|
|
})
|
|
|
|
const channelListHtml = channels
|
|
.map(
|
|
(ch) =>
|
|
`<li><strong>${escapeHtml(ch.name)}</strong> — <code>${escapeHtml(ch.id)}</code>` +
|
|
(ch.country ? ` (${escapeHtml(ch.country)})` : '') +
|
|
(ch.categories.length > 0 ? ` [${ch.categories.map(escapeHtml).join(', ')}]` : '') +
|
|
`</li>`
|
|
)
|
|
.join('\n')
|
|
|
|
const subject =
|
|
channels.length === 1
|
|
? `[Jefflix] Channel Request: ${escapeHtml(channels[0].name)}`
|
|
: `[Jefflix] Channel Request: ${channels.length} channels`
|
|
|
|
// Generate approve token for one-click activation
|
|
const approveToken = createApproveToken(
|
|
channels.map((ch) => ({ id: ch.id, name: ch.name })),
|
|
email,
|
|
)
|
|
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://jefflix.lol'
|
|
const approveUrl = `${baseUrl}/api/approve-channels?token=${approveToken}`
|
|
|
|
await transporter.sendMail({
|
|
from: `Jefflix <${smtpUser}>`,
|
|
to: adminEmail,
|
|
subject,
|
|
html: `
|
|
<h2>New Channel Request</h2>
|
|
<p><strong>${escapeHtml(email)}</strong> requested ${channels.length} channel${channels.length > 1 ? 's' : ''}:</p>
|
|
|
|
<div style="text-align: center; margin: 24px 0;">
|
|
<a href="${approveUrl}" style="display: inline-block; background: #22c55e; color: #fff; padding: 14px 32px; border-radius: 8px; text-decoration: none; font-size: 16px; font-weight: 600;">
|
|
✅ Approve & Add Channels
|
|
</a>
|
|
</div>
|
|
<p style="text-align: center; color: #888; font-size: 12px;">One click activates these channels in Threadfin and notifies the requester. Link expires in 7 days.</p>
|
|
|
|
<ul style="line-height: 1.8;">
|
|
${channelListHtml}
|
|
</ul>
|
|
<hr style="margin: 20px 0; border: none; border-top: 1px solid #ddd;" />
|
|
<p style="color: #666; font-size: 12px;">Automated message from Jefflix · ${new Date().toLocaleString()}</p>
|
|
`,
|
|
})
|
|
|
|
return NextResponse.json({ success: true })
|
|
} catch (error) {
|
|
console.error('Channel request error:', error)
|
|
return NextResponse.json(
|
|
{ error: 'Internal server error' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text: string): string {
|
|
const map: Record<string, string> = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
}
|
|
return text.replace(/[&<>"']/g, (char) => map[char])
|
|
}
|