EventBuzz_Backend/src/controllers/media.controller.js

253 lines
9.6 KiB
JavaScript

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/<orgId>/<eventId>/<module>/...
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 };