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