246 lines
8.3 KiB
JavaScript
246 lines
8.3 KiB
JavaScript
const multer = require('multer')
|
|
const { S3Client, PutObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3')
|
|
const NewsService = require('../../services/news/NewsService')
|
|
const { logger } = require('../../middleware/logger')
|
|
|
|
const PREFIX = 'news/'
|
|
|
|
function buildImageUrlFromKey(key) {
|
|
if (!key) return ''
|
|
const endpoint = process.env.EXOSCALE_ENDPOINT || ''
|
|
const bucket = process.env.EXOSCALE_BUCKET || ''
|
|
if (endpoint.startsWith('http')) {
|
|
return `${endpoint.replace(/\/$/, '')}/${bucket}/${key}`
|
|
}
|
|
return key
|
|
}
|
|
|
|
exports.list = async (req, res) => {
|
|
try {
|
|
const rows = await NewsService.list()
|
|
const data = (rows || []).map(r => ({
|
|
id: r.id,
|
|
title: r.title,
|
|
summary: r.summary,
|
|
content: r.content,
|
|
slug: r.slug,
|
|
category: r.category,
|
|
is_active: !!r.is_active,
|
|
published_at: r.published_at,
|
|
created_at: r.created_at,
|
|
updated_at: r.updated_at,
|
|
imageUrl: r.object_storage_id ? buildImageUrlFromKey(r.object_storage_id) : ''
|
|
}))
|
|
res.json({ success: true, data })
|
|
} catch (e) {
|
|
logger.error('[NewsController.list] error', { msg: e.message })
|
|
res.status(500).json({ error: 'Failed to load news' })
|
|
}
|
|
}
|
|
|
|
exports.listActive = async (req, res) => {
|
|
try {
|
|
const rows = await NewsService.list()
|
|
const data = (rows || [])
|
|
.filter(r => r.is_active)
|
|
.map(r => ({
|
|
id: r.id,
|
|
title: r.title,
|
|
summary: r.summary,
|
|
content: r.content,
|
|
slug: r.slug,
|
|
category: r.category,
|
|
published_at: r.published_at,
|
|
imageUrl: r.object_storage_id ? buildImageUrlFromKey(r.object_storage_id) : ''
|
|
}))
|
|
res.json({ success: true, data })
|
|
} catch (e) {
|
|
logger.error('[NewsController.listActive] error', { msg: e.message })
|
|
res.status(500).json({ error: 'Failed to load news' })
|
|
}
|
|
}
|
|
|
|
exports.create = async (req, res) => {
|
|
try {
|
|
const { title, summary, content, slug, category, isActive, publishedAt } = req.body
|
|
if (!title || !slug) {
|
|
return res.status(400).json({ success: false, error: 'Title and slug are required' })
|
|
}
|
|
let objectKey = null
|
|
let originalFilename = null
|
|
|
|
if (req.file) {
|
|
const allowedMime = ['image/jpeg','image/png','image/webp','image/svg+xml']
|
|
if (!allowedMime.includes(req.file.mimetype)) {
|
|
return res.status(400).json({ success: false, error: 'Invalid image type. Allowed: JPG, PNG, WebP, SVG' })
|
|
}
|
|
if (req.file.size > 5 * 1024 * 1024) {
|
|
return res.status(400).json({ success: false, error: 'Image exceeds 5MB limit' })
|
|
}
|
|
const s3 = new S3Client({
|
|
region: process.env.EXOSCALE_REGION,
|
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
|
credentials: {
|
|
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
|
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
|
|
},
|
|
})
|
|
originalFilename = req.file.originalname
|
|
const key = `${PREFIX}${Date.now()}_${originalFilename.replace(/\s+/g, '_')}`
|
|
await s3.send(new PutObjectCommand({
|
|
Bucket: process.env.EXOSCALE_BUCKET,
|
|
Key: key,
|
|
Body: req.file.buffer,
|
|
ContentType: req.file.mimetype,
|
|
ACL: 'public-read'
|
|
}))
|
|
objectKey = key
|
|
logger.info('[NewsController.create] uploaded image', { key })
|
|
}
|
|
|
|
let id
|
|
try {
|
|
id = await NewsService.create({
|
|
title,
|
|
summary,
|
|
content,
|
|
slug,
|
|
category,
|
|
object_storage_id: objectKey,
|
|
original_filename: originalFilename,
|
|
is_active: isActive ? 1 : 0,
|
|
published_at: publishedAt || null,
|
|
})
|
|
} catch (err) {
|
|
const msg = err && err.message ? err.message : String(err)
|
|
// Handle duplicate slug gracefully
|
|
if (msg.includes('ER_DUP_ENTRY') || msg.toLowerCase().includes('duplicate')) {
|
|
return res.status(409).json({ success: false, error: 'Slug already exists' })
|
|
}
|
|
logger.error('[NewsController.create] db error', { msg })
|
|
return res.status(500).json({ success: false, error: 'Database error while creating news' })
|
|
}
|
|
|
|
res.json({ success: true, data: { id } })
|
|
} catch (e) {
|
|
logger.error('[NewsController.create] error', { msg: e.message })
|
|
res.status(500).json({ success: false, error: e.message || 'Failed to create news' })
|
|
}
|
|
}
|
|
|
|
exports.update = async (req, res) => {
|
|
try {
|
|
const { id } = req.params
|
|
const { title, summary, content, slug, category, isActive, publishedAt, removeImage } = req.body
|
|
const existing = await NewsService.get(id)
|
|
if (!existing) return res.status(404).json({ error: 'Not found' })
|
|
|
|
let objectKey = existing.object_storage_id
|
|
let originalFilename = existing.original_filename
|
|
|
|
if (removeImage && objectKey) {
|
|
try {
|
|
const s3 = new S3Client({
|
|
region: process.env.EXOSCALE_REGION,
|
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
|
credentials: {
|
|
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
|
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
|
|
},
|
|
})
|
|
await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: objectKey }))
|
|
} catch (err) {
|
|
logger.error('[NewsController.update] delete image error', { msg: err.message })
|
|
}
|
|
objectKey = null
|
|
originalFilename = null
|
|
}
|
|
|
|
if (req.file) {
|
|
const allowedMime = ['image/jpeg','image/png','image/webp','image/svg+xml']
|
|
if (!allowedMime.includes(req.file.mimetype)) {
|
|
return res.status(400).json({ success: false, error: 'Invalid image type. Allowed: JPG, PNG, WebP, SVG' })
|
|
}
|
|
if (req.file.size > 5 * 1024 * 1024) {
|
|
return res.status(400).json({ success: false, error: 'Image exceeds 5MB limit' })
|
|
}
|
|
const s3 = new S3Client({
|
|
region: process.env.EXOSCALE_REGION,
|
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
|
credentials: {
|
|
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
|
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
|
|
},
|
|
})
|
|
const newKey = `${PREFIX}${Date.now()}_${req.file.originalname.replace(/\s+/g, '_')}`
|
|
await s3.send(new PutObjectCommand({
|
|
Bucket: process.env.EXOSCALE_BUCKET,
|
|
Key: newKey,
|
|
Body: req.file.buffer,
|
|
ContentType: req.file.mimetype,
|
|
ACL: 'public-read'
|
|
}))
|
|
objectKey = newKey
|
|
originalFilename = req.file.originalname
|
|
logger.info('[NewsController.update] uploaded new image', { key: newKey })
|
|
}
|
|
|
|
await NewsService.update(id, {
|
|
title,
|
|
summary,
|
|
content,
|
|
slug,
|
|
category,
|
|
object_storage_id: objectKey,
|
|
original_filename: originalFilename,
|
|
is_active: isActive !== undefined ? (isActive ? 1 : 0) : undefined,
|
|
published_at: publishedAt !== undefined ? (publishedAt || null) : undefined,
|
|
})
|
|
|
|
res.json({ success: true })
|
|
} catch (e) {
|
|
logger.error('[NewsController.update] error', { msg: e.message })
|
|
res.status(500).json({ error: 'Failed to update news' })
|
|
}
|
|
}
|
|
|
|
exports.updateStatus = async (req, res) => {
|
|
try {
|
|
const { id } = req.params
|
|
const { isActive } = req.body
|
|
await NewsService.update(id, { is_active: isActive ? 1 : 0 })
|
|
res.json({ success: true })
|
|
} catch (e) {
|
|
logger.error('[NewsController.updateStatus] error', { msg: e.message })
|
|
res.status(500).json({ error: 'Failed to update status' })
|
|
}
|
|
}
|
|
|
|
exports.delete = async (req, res) => {
|
|
try {
|
|
const { id } = req.params
|
|
const existing = await NewsService.get(id)
|
|
if (!existing) return res.status(404).json({ error: 'Not found' })
|
|
if (existing.object_storage_id) {
|
|
try {
|
|
const s3 = new S3Client({
|
|
region: process.env.EXOSCALE_REGION,
|
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
|
credentials: {
|
|
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
|
secretAccessKey: process.env.EXOSCALE_SECRET_KEY,
|
|
},
|
|
})
|
|
await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: existing.object_storage_id }))
|
|
} catch (err) {
|
|
logger.error('[NewsController.delete] delete image error', { msg: err.message })
|
|
}
|
|
}
|
|
await NewsService.delete(id)
|
|
res.json({ success: true })
|
|
} catch (e) {
|
|
logger.error('[NewsController.delete] error', { msg: e.message })
|
|
res.status(500).json({ error: 'Failed to delete news' })
|
|
}
|
|
}
|