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, slug: r.slug, category: r.category, is_active: !!r.is_active, 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.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, 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' }) } }