CentralBackend/controller/news/NewsController.js
2025-12-14 18:20:47 +01:00

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