initial commit
This commit is contained in:
427
app/api/topic/route.ts
Normal file
427
app/api/topic/route.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user