135 lines
4.5 KiB
JavaScript
135 lines
4.5 KiB
JavaScript
import express from 'express'
|
|
import { getPresignedUploadUrl, listBlogImages, deleteBlogImage } from '../config/s3.js'
|
|
import logger from '../utils/logger.js'
|
|
|
|
const router = express.Router()
|
|
|
|
// List media for a blog (Media Library)
|
|
// GET /upload/media?postId=xxx or ?sessionId=xxx
|
|
router.get('/media', async (req, res) => {
|
|
try {
|
|
const { postId, sessionId } = req.query
|
|
if (!postId && !sessionId) {
|
|
return res.status(400).json({ message: 'postId or sessionId is required' })
|
|
}
|
|
const items = await listBlogImages(postId || null, sessionId || null)
|
|
res.json({ items })
|
|
} catch (error) {
|
|
logger.error('UPLOAD', 'Error listing media', error)
|
|
res.status(500).json({ message: error.message || 'Failed to list media' })
|
|
}
|
|
})
|
|
|
|
// Get presigned URL for image upload
|
|
// Note: authenticateToken middleware is applied at server level
|
|
router.post('/presigned-url', async (req, res) => {
|
|
try {
|
|
const { filename, contentType, postId, sessionId, purpose } = req.body
|
|
|
|
logger.transaction('GENERATE_PRESIGNED_URL', {
|
|
userId: req.user.id,
|
|
filename,
|
|
contentType,
|
|
postId,
|
|
sessionId,
|
|
purpose
|
|
})
|
|
|
|
if (!filename || !contentType) {
|
|
logger.warn('UPLOAD', 'Missing required fields', {
|
|
hasFilename: !!filename,
|
|
hasContentType: !!contentType
|
|
})
|
|
return res.status(400).json({ message: 'Filename and content type are required' })
|
|
}
|
|
|
|
// Thumbnail upload requires postId (post must be saved first)
|
|
if (purpose === 'thumbnail' && !postId) {
|
|
return res.status(400).json({
|
|
message: 'Post ID is required to upload a thumbnail. Save the post as draft first.'
|
|
})
|
|
}
|
|
|
|
// Validate content type
|
|
if (!contentType.startsWith('image/')) {
|
|
logger.warn('UPLOAD', 'Invalid content type', { contentType })
|
|
return res.status(400).json({ message: 'File must be an image' })
|
|
}
|
|
|
|
// Check if AWS credentials are configured
|
|
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
|
|
logger.error('UPLOAD', 'AWS credentials not configured', null)
|
|
return res.status(500).json({
|
|
message: 'AWS S3 is not configured. Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in backend .env file.'
|
|
})
|
|
}
|
|
|
|
// Support both S3_BUCKET_NAME and AWS_BUCKET_NAME (for compatibility with api-v1)
|
|
const bucketName = process.env.S3_BUCKET_NAME || process.env.AWS_BUCKET_NAME
|
|
if (!bucketName) {
|
|
logger.error('UPLOAD', 'S3 bucket name not configured', null)
|
|
return res.status(500).json({
|
|
message: 'S3 bucket name is not configured. Please set S3_BUCKET_NAME or AWS_BUCKET_NAME in backend .env file.'
|
|
})
|
|
}
|
|
|
|
const startTime = Date.now()
|
|
const { uploadUrl, imageUrl, key } = await getPresignedUploadUrl(filename, contentType, postId, sessionId, purpose)
|
|
const duration = Date.now() - startTime
|
|
|
|
logger.s3('PRESIGNED_URL_GENERATED', {
|
|
userId: req.user.id,
|
|
filename,
|
|
key,
|
|
duration: `${duration}ms`
|
|
})
|
|
|
|
logger.transaction('GENERATE_PRESIGNED_URL_SUCCESS', {
|
|
userId: req.user.id,
|
|
key,
|
|
duration: `${duration}ms`
|
|
})
|
|
|
|
res.json({
|
|
uploadUrl,
|
|
imageUrl,
|
|
})
|
|
} catch (error) {
|
|
logger.error('UPLOAD', 'Error generating presigned URL', error)
|
|
|
|
// Provide more specific error messages
|
|
let errorMessage = 'Failed to generate upload URL'
|
|
if (error.name === 'InvalidAccessKeyId' || error.name === 'SignatureDoesNotMatch') {
|
|
errorMessage = 'Invalid AWS credentials. Please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.'
|
|
} else if (error.name === 'NoSuchBucket') {
|
|
const bucketName = process.env.S3_BUCKET_NAME || process.env.AWS_BUCKET_NAME
|
|
errorMessage = `S3 bucket '${bucketName}' does not exist. Please create it or update S3_BUCKET_NAME/AWS_BUCKET_NAME.`
|
|
} else if (error.message) {
|
|
errorMessage = error.message
|
|
}
|
|
|
|
res.status(500).json({
|
|
message: errorMessage,
|
|
error: error.name || 'Unknown error'
|
|
})
|
|
}
|
|
})
|
|
|
|
// Delete image from S3 (Media Library)
|
|
// DELETE /upload/media with body { key: "blogs/123/images/uuid.jpg" }
|
|
router.delete('/media', async (req, res) => {
|
|
try {
|
|
const { key } = req.body
|
|
if (!key) {
|
|
return res.status(400).json({ message: 'key is required' })
|
|
}
|
|
await deleteBlogImage(key)
|
|
res.status(204).end()
|
|
} catch (error) {
|
|
logger.error('UPLOAD', 'Error deleting media', error)
|
|
res.status(500).json({ message: error.message || 'Failed to delete from storage' })
|
|
}
|
|
})
|
|
|
|
export default router
|