const mongoose = require('mongoose'); const Content = require('../models/Content'); const LibraryContent = require('../models/LibraryContent'); const ContentCollection = require('../models/ContentCollection'); const LIBRARY_MODULE = 'content-library'; const { success, error } = require('../utils/response'); const { resolveScopedOrganizationId } = require('../utils/tenantListScope'); const { uploadBufferToS3AndGetCloudFrontUrl, deleteS3ObjectByKey } = require('../utils/s3Media'); function parseTagsFromPayload(payload) { const raw = payload.tags; if (raw == null) return []; if (Array.isArray(raw)) return raw.map((t) => String(t).trim()).filter(Boolean); if (typeof raw === 'string') { const s = raw.trim(); if (!s) return []; try { const j = JSON.parse(s); if (Array.isArray(j)) return j.map((t) => String(t).trim()).filter(Boolean); } catch { /* comma-separated */ } return s.split(/[,;\n]+/).map((t) => t.trim()).filter(Boolean); } return []; } function inferContentType(mimeType, payloadType) { if (payloadType && ['image', 'pdf', 'video', 'document', 'other'].includes(payloadType)) return payloadType; const m = String(mimeType || '').toLowerCase(); if (m.startsWith('image/')) return 'image'; if (m.startsWith('video/')) return 'video'; if (m === 'application/pdf') return 'pdf'; if (m.startsWith('text/') || m.includes('document') || m.includes('msword')) return 'document'; return 'document'; } function normalizeVisibility(v) { const s = String(v || '').toLowerCase(); if (s === 'public') return 'public'; return 'private'; } /** * Hapi route payload.output only allows data|stream|file. With output:'data', binary file parts are often * a raw Buffer on payload.file (see @hapi/subtext internals.part). Some clients may send { _data } or { payload }. */ function extractUploadFile(payload) { const file = payload.file; if (!file) return null; if (Buffer.isBuffer(file)) { return { buffer: file, filename: (typeof payload.name === 'string' && payload.name.trim()) || 'file', mimeType: 'application/octet-stream', }; } if (typeof file === 'object' && file.payload && Buffer.isBuffer(file.payload)) { return { buffer: file.payload, filename: file.filename || (typeof payload.name === 'string' && payload.name) || 'file', mimeType: file.headers?.['content-type'] || 'application/octet-stream', }; } if (typeof file === 'object' && file._data && Buffer.isBuffer(file._data)) { return { buffer: file._data, filename: file.filename || (typeof payload.name === 'string' && payload.name) || 'file', mimeType: file.headers?.['content-type'] || 'application/octet-stream', }; } return null; } function listOrgFilter(user, query) { const scoped = resolveScopedOrganizationId(user, query.organization); if (scoped.error) return { error: scoped.error }; return { filter: { organization: scoped.organizationId } }; } /** Non-library uploads (events, speakers, etc.) — `Content` collection. */ const list = async (request, h) => { const user = request.auth.credentials.user; const orgRes = listOrgFilter(user, request.query); if (orgRes.error) return h.response(error(orgRes.error.code, orgRes.error.message)).code(400); const filter = { ...orgRes.filter }; if (request.query.type) filter.type = request.query.type; if (request.query.collectionName) filter.collectionName = request.query.collectionName; if (request.query.eventId || request.query.event_id) filter.eventId = request.query.eventId || request.query.event_id; const { page = 1, limit = 20 } = request.query; const skip = (parseInt(page, 10) - 1) * parseInt(limit, 10); const [media, total] = await Promise.all([ Content.find(filter).populate({ path: 'uploadedBy', select: 'firstName lastName email phone' }).skip(skip).limit(parseInt(limit, 10)).lean(), Content.countDocuments(filter), ]); return h.response(success({ media, total, page: parseInt(page, 10), limit: parseInt(limit, 10) })) .header('X-Total-Count', total); }; /** Portfolio → Library only — `LibraryContent` collection. */ const listLibrary = async (request, h) => { const user = request.auth.credentials.user; const orgRes = listOrgFilter(user, request.query); if (orgRes.error) return h.response(error(orgRes.error.code, orgRes.error.message)).code(400); const filter = { ...orgRes.filter }; if (request.query.type) filter.type = request.query.type; const { page = 1, limit = 20 } = request.query; const skip = (parseInt(page, 10) - 1) * parseInt(limit, 10); const [media, total] = await Promise.all([ LibraryContent.find(filter).populate({ path: 'uploadedBy', select: 'firstName lastName email phone' }).sort({ createdAt: -1 }).skip(skip).limit(parseInt(limit, 10)).lean(), LibraryContent.countDocuments(filter), ]); return h.response(success({ media, total, page: parseInt(page, 10), limit: parseInt(limit, 10) })) .header('X-Total-Count', total); }; const upload = async (request, h) => { const user = request.auth.credentials.user; const orgId = user.organization?._id || user.organization; if (!orgId) return h.response(error('VALIDATION_ERROR', 'Organization required')).code(400); const payload = request.payload; const extracted = extractUploadFile(payload); // Required by your storage scheme: eventbuzz////... const eventId = payload.eventId || payload.event_id || payload.event || ''; const module = payload.module || payload.collectionName || 'general'; const isLibraryUpload = String(module) === LIBRARY_MODULE; let url = payload.url || ''; let size = 0; let mimeType = ''; let s3Key = ''; if (extracted) { const { buffer, filename: origName, mimeType: ct } = extracted; const filename = `${Date.now()}-${(origName || 'file').replace(/[^a-zA-Z0-9.-]/g, '_')}`; mimeType = ct || 'application/octet-stream'; const uploaded = await uploadBufferToS3AndGetCloudFrontUrl({ buffer, contentType: mimeType, orgId, eventId, module, filename, }); url = uploaded.url; s3Key = uploaded.s3Key; size = buffer.length; } else if (!payload.url) { return h .response( error( 'VALIDATION_ERROR', 'No file received. Send multipart/form-data with a field named "file" (binary). Do not use JSON body for uploads.', ), ) .code(400); } const collRaw = payload.contentCollectionId || payload.content_collection_id; if ( isLibraryUpload && collRaw && mongoose.Types.ObjectId.isValid(String(collRaw)) ) { const collId = new mongoose.Types.ObjectId(String(collRaw)); const collOk = await ContentCollection.exists({ _id: collId, organization: orgId }); if (!collOk) { return h.response(error('VALIDATION_ERROR', 'Invalid content collection for this organization')).code(400); } } const type = inferContentType(mimeType, payload.type); const tags = parseTagsFromPayload(payload); const description = typeof payload.description === 'string' ? payload.description.trim() : ''; const visibility = normalizeVisibility(payload.visibility); const baseDoc = { organization: orgId, eventId: eventId || undefined, name: payload.name || extracted?.filename || 'Untitled', description, visibility, type, url, size, mimeType, tags, collectionName: payload.collectionName, s3Key, uploadedBy: user._id, }; const media = isLibraryUpload ? await LibraryContent.create(baseDoc) : await Content.create(baseDoc); if (collRaw && mongoose.Types.ObjectId.isValid(String(collRaw)) && isLibraryUpload) { const collId = new mongoose.Types.ObjectId(String(collRaw)); await ContentCollection.updateOne({ _id: collId, organization: orgId }, { $addToSet: { contentIds: media._id } }); } return h.response(success(media, 'File uploaded', 201)).code(201); }; const get = async (request, h) => { let media = await Content.findById(request.params.id); if (!media) media = await LibraryContent.findById(request.params.id); if (!media) return h.response(error('NOT_FOUND', 'File not found')).code(404); const user = request.auth.credentials.user; if (user.role !== 'super_admin' && media.organization?.toString() !== (user.organization?._id || user.organization)?.toString()) { return h.response(error('FORBIDDEN', 'Access denied')).code(403); } return h.response(success(media)); }; const remove = async (request, h) => { let media = await Content.findById(request.params.id); if (!media) media = await LibraryContent.findById(request.params.id); if (!media) return h.response(error('NOT_FOUND', 'File not found')).code(404); const user = request.auth.credentials.user; if (user.role !== 'super_admin' && media.organization?.toString() !== (user.organization?._id || user.organization)?.toString()) { return h.response(error('FORBIDDEN', 'Access denied')).code(403); } // Prefer S3 deletion if we have the key, otherwise fall back to legacy local /uploads deletion. if (media.s3Key) { await deleteS3ObjectByKey({ s3Key: media.s3Key }); } else if (media.url && media.url.startsWith('/uploads/')) { // Legacy fallback (optional): local file deletion if still present. try { const path = require('path'); const fs = require('fs'); const filepath = path.join(process.cwd(), media.url); if (fs.existsSync(filepath)) fs.unlinkSync(filepath); } catch {} } const id = media._id; await media.deleteOne(); await ContentCollection.updateMany({ contentIds: id }, { $pull: { contentIds: id } }); return h.response(success(null, 'File deleted')).code(200); }; module.exports = { list, listLibrary, upload, get, remove };