ai-wpa/app/api/topic/route.ts

428 lines
13 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { randomUUID } from 'crypto'
import TopicModel from '@/models/topic'
import { authMiddleware } from '@/lib/auth-middleware'
import { uploadFile, moveToPermStorage, getFileUrl, generateUniqueFilename } from '@/lib/file-vault'
export async function POST(request: NextRequest) {
try {
console.log('Starting topic post processing')
// Debug authentication data
const authHeader = request.headers.get('authorization')
const accessTokenCookie = request.cookies.get('accessToken')?.value
console.log('🔍 Auth debug:', {
hasAuthHeader: !!authHeader,
authHeaderPrefix: authHeader?.substring(0, 20),
hasAccessTokenCookie: !!accessTokenCookie,
cookiePrefix: accessTokenCookie?.substring(0, 20),
allCookies: Object.fromEntries(
request.cookies.getAll().map((c) => [c.name, c.value?.substring(0, 20)])
),
})
// Authentication required
const user = await authMiddleware(request)
console.log('🔍 Auth result:', { user: user ? { id: user.id, email: user.email } : null })
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
},
{ status: 401 }
)
}
const formData = await request.formData()
const coverImage = formData.get('coverImage') as File
const topicId = request.nextUrl.searchParams.get('topicId')
console.log('Form data received', { topicId, hasCoverImage: !!coverImage })
// Get other form data with proper validation
const title = formData.get('title') as string
const author = formData.get('author') as string
const excerpt = formData.get('excerpt') as string
const content = formData.get('content') as string
const contentRTE = formData.get('contentRTE') as string
const featured = formData.get('featured') === 'true'
// Get the LAST isDraft value (in case of duplicates)
const allIsDraftValues = formData.getAll('isDraft')
console.log('🐛 All isDraft values in FormData:', allIsDraftValues)
const isDraft = allIsDraftValues[allIsDraftValues.length - 1] === 'true'
console.log('🐛 Final isDraft value:', isDraft)
// Parse JSON fields safely (like dev-portfolio)
let contentImages: string[] = []
let tags: any[] = []
try {
const contentImagesStr = formData.get('contentImages') as string
contentImages = contentImagesStr ? JSON.parse(contentImagesStr) : []
} catch (error) {
console.warn('Failed to parse contentImages, using empty array:', error)
contentImages = []
}
try {
const tagsStr = formData.get('tags') as string
tags = tagsStr ? JSON.parse(tagsStr) : []
} catch (error) {
console.warn('Failed to parse tags, using empty array:', error)
tags = []
}
// Validate required fields (like dev-portfolio)
if (!title?.trim()) {
return NextResponse.json(
{ success: false, error: { message: 'Title is required', code: 'VALIDATION_ERROR' } },
{ status: 400 }
)
}
if (!excerpt?.trim()) {
return NextResponse.json(
{ success: false, error: { message: 'Excerpt is required', code: 'VALIDATION_ERROR' } },
{ status: 400 }
)
}
if (!content?.trim()) {
return NextResponse.json(
{ success: false, error: { message: 'Content is required', code: 'VALIDATION_ERROR' } },
{ status: 400 }
)
}
if (!Array.isArray(tags) || tags.length === 0) {
return NextResponse.json(
{
success: false,
error: { message: 'At least one tag is required', code: 'VALIDATION_ERROR' },
},
{ status: 400 }
)
}
const targetTopicId = topicId ? topicId : randomUUID()
// For existing topic, fetch the current data
let existingTopic = null
if (topicId) {
existingTopic = await TopicModel.findOne({ id: topicId })
if (!existingTopic) {
return NextResponse.json({ success: false, message: 'Topic not found' }, { status: 404 })
}
}
// Handle cover image upload exactly like dev-portfolio
let tempImageResult = null
if (coverImage && coverImage instanceof File) {
try {
console.log('🖼️ Processing cover image upload:', {
name: coverImage.name,
size: coverImage.size,
type: coverImage.type,
})
// Upload using external API
try {
// Convert file to buffer
const bytes = await coverImage.arrayBuffer()
const buffer = Buffer.from(bytes)
console.log('📤 Buffer created, starting upload...')
// Upload directly to external API (no temp storage needed)
const uploadResult = await uploadFile(buffer, coverImage.name, coverImage.type, user.id)
const coverImageUrl = uploadResult.url
const permanentPath = uploadResult.url
tempImageResult = {
coverImage: coverImageUrl,
coverImageKey: permanentPath,
}
console.log('✅ Cover image uploaded successfully:', { permanentPath })
} catch (uploadError) {
console.error('❌ Cover image upload failed:', uploadError)
throw new Error(`Failed to upload cover image: ${uploadError.message}`)
}
} catch (error) {
console.error('Error processing cover image:', error)
}
}
const dataToSave = {
id: targetTopicId,
publishedAt: Date.now(),
title,
author,
authorId: user.id, // Add user ownership
excerpt,
content,
contentRTE: contentRTE ? JSON.parse(contentRTE) : [],
contentImages,
featured,
tags,
isDraft,
// Use uploaded image or existing image for updates, require image for new topics like dev-portfolio
coverImage:
tempImageResult?.coverImage ||
existingTopic?.coverImage ||
'https://via.placeholder.com/800x400',
coverImageKey: tempImageResult?.coverImageKey || existingTopic?.coverImageKey || '',
}
console.log('Preparing to save topic data', { targetTopicId })
let savedArticle
try {
if (existingTopic) {
// Update existing topic
console.log('Updating existing topic')
Object.assign(existingTopic, dataToSave)
savedArticle = await existingTopic.save()
} else {
// Create new topic
console.log('Creating new topic')
const newArticle = new TopicModel(dataToSave)
savedArticle = await newArticle.save()
}
console.log('Topic saved successfully', { topicId: savedArticle.id })
} catch (error) {
console.error('Failed to save topic', { error })
throw error
}
return NextResponse.json(
{
success: true,
data: savedArticle,
},
{ status: 200 }
)
} catch (error) {
console.error('Error processing topic post:', {
error,
message: error.message,
})
return NextResponse.json(
{ success: false, message: 'Failed to process topic post', error: error.message },
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const featured = searchParams.get('featured') === 'true'
const isDraft = searchParams.get('isDraft')
const search = searchParams.get('search')
const tag = searchParams.get('tag')
// Build query (like dev-portfolio)
const query: any = {}
// Featured filter
if (featured) query.featured = true
// Draft filter (only if explicitly set)
if (isDraft !== null) query.isDraft = isDraft === 'true'
// Search functionality (like dev-portfolio)
if (search) {
query.$or = [
{ title: { $regex: search, $options: 'i' } },
{ excerpt: { $regex: search, $options: 'i' } },
{ content: { $regex: search, $options: 'i' } },
]
}
// Tag filtering (like dev-portfolio)
if (tag) {
query['tags.name'] = { $regex: tag, $options: 'i' }
}
const skip = (page - 1) * limit
const [topics, total] = await Promise.all([
TopicModel.find(query).sort({ publishedAt: -1 }).skip(skip).limit(limit),
TopicModel.countDocuments(query),
])
return NextResponse.json({
success: true,
data: {
topics,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
},
})
} catch (error) {
console.error('Error fetching topics:', error)
return NextResponse.json(
{
success: false,
error: { message: 'Failed to fetch topics', code: 'SERVER_ERROR' },
},
{ status: 500 }
)
}
}
// PUT /api/topic - Update existing topic (like dev-portfolio)
export async function PUT(request: NextRequest) {
try {
console.log('Starting topic update processing')
// Authentication required
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
},
{ status: 401 }
)
}
const formData = await request.formData()
const topicId = formData.get('topicId') as string
if (!topicId) {
return NextResponse.json(
{
success: false,
error: { message: 'Topic ID is required for updates', code: 'MISSING_TOPIC_ID' },
},
{ status: 400 }
)
}
// Find existing topic and check ownership
const existingTopic = await TopicModel.findOne({ id: topicId })
if (!existingTopic) {
return NextResponse.json({ success: false, message: 'Topic not found' }, { status: 404 })
}
// Check authorization - user can only update their own topics
if (existingTopic.authorId !== user.id) {
return NextResponse.json(
{
success: false,
error: { message: 'Not authorized to update this topic', code: 'UNAUTHORIZED' },
},
{ status: 403 }
)
}
// Use the same logic as POST but for updates
// This reuses the form processing logic from POST
const url = new URL(request.url)
url.searchParams.set('topicId', topicId)
// Create new request for POST handler with topicId parameter
const updateRequest = new NextRequest(url.toString(), {
method: 'POST',
body: formData,
headers: request.headers,
})
return POST(updateRequest)
} catch (error) {
console.error('Error updating topic post:', error)
return NextResponse.json(
{ success: false, message: 'Failed to update topic post', error: error.message },
{ status: 500 }
)
}
}
// DELETE /api/topic - Delete topic (like dev-portfolio)
export async function DELETE(request: NextRequest) {
try {
console.log('Starting topic deletion')
// Authentication required
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'AUTH_REQUIRED' },
},
{ status: 401 }
)
}
const { searchParams } = new URL(request.url)
const topicId = searchParams.get('id')
if (!topicId) {
return NextResponse.json(
{ success: false, error: { message: 'Topic ID is required', code: 'MISSING_TOPIC_ID' } },
{ status: 400 }
)
}
// Find existing topic and check ownership
const existingTopic = await TopicModel.findOne({ id: topicId })
if (!existingTopic) {
return NextResponse.json({ success: false, message: 'Topic not found' }, { status: 404 })
}
// Check authorization - user can only delete their own topics
if (existingTopic.authorId !== user.id) {
return NextResponse.json(
{
success: false,
error: { message: 'Not authorized to delete this topic', code: 'UNAUTHORIZED' },
},
{ status: 403 }
)
}
console.log('Deleting topic:', { topicId, title: existingTopic.title?.substring(0, 50) })
// Delete the topic
await TopicModel.deleteOne({ id: topicId })
console.log('Topic deleted successfully:', topicId)
// TODO: Implement image cleanup like dev-portfolio
// if (existingTopic.coverImageKey && existingTopic.coverImageKey !== 'placeholder-key') {
// await deleteFile(existingTopic.coverImageKey)
// }
// if (existingTopic.contentImages && existingTopic.contentImages.length > 0) {
// for (const imagePath of existingTopic.contentImages) {
// await deleteFile(imagePath)
// }
// }
return NextResponse.json(
{
success: true,
message: 'Topic deleted successfully',
data: { id: topicId },
},
{ status: 200 }
)
} catch (error) {
console.error('Error deleting topic:', error)
return NextResponse.json(
{ success: false, message: 'Failed to delete topic', error: error.message },
{ status: 500 }
)
}
}