| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 | import { Meteor } from 'meteor/meteor';import { WebApp } from 'meteor/webapp';import { ReactiveCache } from '/imports/reactiveCache';import { Attachments, fileStoreStrategyFactory } from '/models/attachments';import { moveToStorage } from '/models/lib/fileStoreStrategy';import { STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS, STORAGE_NAME_S3 } from '/models/lib/fileStoreStrategy';import AttachmentStorageSettings from '/models/attachmentStorageSettings';import fs from 'fs';import path from 'path';import { ObjectID } from 'bson';// Attachment API HTTP routesif (Meteor.isServer) {  // Helper function to authenticate API requests  function authenticateApiRequest(req) {    const authHeader = req.headers.authorization;    if (!authHeader || !authHeader.startsWith('Bearer ')) {      throw new Meteor.Error('unauthorized', 'Missing or invalid authorization header');    }    const token = authHeader.substring(7);    // Here you would validate the token and get the user ID    // For now, we'll use a simple approach - in production, you'd want proper JWT validation    const userId = token; // This should be replaced with proper token validation        if (!userId) {      throw new Meteor.Error('unauthorized', 'Invalid token');    }    return userId;  }  // Helper function to send JSON response  function sendJsonResponse(res, statusCode, data) {    res.writeHead(statusCode, { 'Content-Type': 'application/json' });    res.end(JSON.stringify(data));  }  // Helper function to send error response  function sendErrorResponse(res, statusCode, message) {    sendJsonResponse(res, statusCode, { success: false, error: message });  }  // Upload attachment endpoint  WebApp.connectHandlers.use('/api/attachment/upload', (req, res, next) => {    if (req.method !== 'POST') {      return next();    }    try {      const userId = authenticateApiRequest(req);            let body = '';      req.on('data', chunk => {        body += chunk.toString();      });      req.on('end', () => {        try {          const data = JSON.parse(body);          const { boardId, swimlaneId, listId, cardId, fileData, fileName, fileType, storageBackend } = data;          // Validate parameters          if (!boardId || !swimlaneId || !listId || !cardId || !fileData || !fileName) {            return sendErrorResponse(res, 400, 'Missing required parameters');          }          // Check if user has permission to modify the card          const card = ReactiveCache.getCard(cardId);          if (!card) {            return sendErrorResponse(res, 404, 'Card not found');          }          const board = ReactiveCache.getBoard(boardId);          if (!board) {            return sendErrorResponse(res, 404, 'Board not found');          }          // Check permissions          if (!board.isBoardMember(userId)) {            return sendErrorResponse(res, 403, 'You do not have permission to modify this card');          }          // Check if board allows attachments          if (!board.allowsAttachments) {            return sendErrorResponse(res, 403, 'Attachments are not allowed on this board');          }          // Get default storage backend if not specified          let targetStorage = storageBackend;          if (!targetStorage) {            try {              const settings = AttachmentStorageSettings.findOne({});              targetStorage = settings ? settings.getDefaultStorage() : STORAGE_NAME_FILESYSTEM;            } catch (error) {              targetStorage = STORAGE_NAME_FILESYSTEM;            }          }          // Validate storage backend          if (![STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS, STORAGE_NAME_S3].includes(targetStorage)) {            return sendErrorResponse(res, 400, 'Invalid storage backend');          }          // Create file object from base64 data          const fileBuffer = Buffer.from(fileData, 'base64');          const file = new File([fileBuffer], fileName, { type: fileType || 'application/octet-stream' });          // Create attachment metadata          const fileId = new ObjectID().toString();          const meta = {            boardId: boardId,            swimlaneId: swimlaneId,            listId: listId,            cardId: cardId,            fileId: fileId,            source: 'api',            storageBackend: targetStorage          };          // Create attachment          const uploader = Attachments.insert({            file: file,            meta: meta,            isBase64: false,            transport: 'http'          });          if (uploader) {            // Move to target storage if not filesystem            if (targetStorage !== STORAGE_NAME_FILESYSTEM) {              Meteor.defer(() => {                try {                  moveToStorage(uploader, targetStorage, fileStoreStrategyFactory);                } catch (error) {                  console.error('Error moving attachment to target storage:', error);                }              });            }            sendJsonResponse(res, 200, {              success: true,              attachmentId: uploader._id,              fileName: fileName,              fileSize: fileBuffer.length,              storageBackend: targetStorage,              message: 'Attachment uploaded successfully'            });          } else {            sendErrorResponse(res, 500, 'Failed to upload attachment');          }        } catch (error) {          console.error('API attachment upload error:', error);          sendErrorResponse(res, 500, error.message);        }      });    } catch (error) {      sendErrorResponse(res, 401, error.message);    }  });  // Download attachment endpoint  WebApp.connectHandlers.use('/api/attachment/download/([^/]+)', (req, res, next) => {    if (req.method !== 'GET') {      return next();    }    try {      const userId = authenticateApiRequest(req);      const attachmentId = req.params[0];      // Get attachment      const attachment = ReactiveCache.getAttachment(attachmentId);      if (!attachment) {        return sendErrorResponse(res, 404, 'Attachment not found');      }      // Check permissions      const board = ReactiveCache.getBoard(attachment.meta.boardId);      if (!board || !board.isBoardMember(userId)) {        return sendErrorResponse(res, 403, 'You do not have permission to access this attachment');      }      // Get file strategy      const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');      const readStream = strategy.getReadStream();      if (!readStream) {        return sendErrorResponse(res, 404, 'File not found in storage');      }      // Read file data      const chunks = [];      readStream.on('data', (chunk) => {        chunks.push(chunk);      });      readStream.on('end', () => {        const fileBuffer = Buffer.concat(chunks);        const base64Data = fileBuffer.toString('base64');                sendJsonResponse(res, 200, {          success: true,          attachmentId: attachmentId,          fileName: attachment.name,          fileSize: attachment.size,          fileType: attachment.type,          base64Data: base64Data,          storageBackend: strategy.getStorageName()        });      });      readStream.on('error', (error) => {        console.error('Download error:', error);        sendErrorResponse(res, 500, error.message);      });    } catch (error) {      sendErrorResponse(res, 401, error.message);    }  });  // List attachments endpoint  WebApp.connectHandlers.use('/api/attachment/list/([^/]+)/([^/]+)/([^/]+)/([^/]+)', (req, res, next) => {    if (req.method !== 'GET') {      return next();    }    try {      const userId = authenticateApiRequest(req);      const boardId = req.params[0];      const swimlaneId = req.params[1];      const listId = req.params[2];      const cardId = req.params[3];      // Check permissions      const board = ReactiveCache.getBoard(boardId);      if (!board || !board.isBoardMember(userId)) {        return sendErrorResponse(res, 403, 'You do not have permission to access this board');      }      let query = { 'meta.boardId': boardId };      if (swimlaneId && swimlaneId !== 'null') {        query['meta.swimlaneId'] = swimlaneId;      }      if (listId && listId !== 'null') {        query['meta.listId'] = listId;      }      if (cardId && cardId !== 'null') {        query['meta.cardId'] = cardId;      }      const attachments = ReactiveCache.getAttachments(query);            const attachmentList = attachments.map(attachment => {        const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');        return {          attachmentId: attachment._id,          fileName: attachment.name,          fileSize: attachment.size,          fileType: attachment.type,          storageBackend: strategy.getStorageName(),          boardId: attachment.meta.boardId,          swimlaneId: attachment.meta.swimlaneId,          listId: attachment.meta.listId,          cardId: attachment.meta.cardId,          createdAt: attachment.uploadedAt,          isImage: attachment.isImage        };      });      sendJsonResponse(res, 200, {        success: true,        attachments: attachmentList,        count: attachmentList.length      });    } catch (error) {      sendErrorResponse(res, 401, error.message);    }  });  // Copy attachment endpoint  WebApp.connectHandlers.use('/api/attachment/copy', (req, res, next) => {    if (req.method !== 'POST') {      return next();    }    try {      const userId = authenticateApiRequest(req);            let body = '';      req.on('data', chunk => {        body += chunk.toString();      });      req.on('end', () => {        try {          const data = JSON.parse(body);          const { attachmentId, targetBoardId, targetSwimlaneId, targetListId, targetCardId } = data;          // Get source attachment          const sourceAttachment = ReactiveCache.getAttachment(attachmentId);          if (!sourceAttachment) {            return sendErrorResponse(res, 404, 'Source attachment not found');          }          // Check source permissions          const sourceBoard = ReactiveCache.getBoard(sourceAttachment.meta.boardId);          if (!sourceBoard || !sourceBoard.isBoardMember(userId)) {            return sendErrorResponse(res, 403, 'You do not have permission to access the source attachment');          }          // Check target permissions          const targetBoard = ReactiveCache.getBoard(targetBoardId);          if (!targetBoard || !targetBoard.isBoardMember(userId)) {            return sendErrorResponse(res, 403, 'You do not have permission to modify the target card');          }          // Check if target board allows attachments          if (!targetBoard.allowsAttachments) {            return sendErrorResponse(res, 403, 'Attachments are not allowed on the target board');          }          // Get source file strategy          const sourceStrategy = fileStoreStrategyFactory.getFileStrategy(sourceAttachment, 'original');          const readStream = sourceStrategy.getReadStream();          if (!readStream) {            return sendErrorResponse(res, 404, 'Source file not found in storage');          }          // Read source file data          const chunks = [];          readStream.on('data', (chunk) => {            chunks.push(chunk);          });          readStream.on('end', () => {            try {              const fileBuffer = Buffer.concat(chunks);              const file = new File([fileBuffer], sourceAttachment.name, { type: sourceAttachment.type });              // Create new attachment metadata              const fileId = new ObjectID().toString();              const meta = {                boardId: targetBoardId,                swimlaneId: targetSwimlaneId,                listId: targetListId,                cardId: targetCardId,                fileId: fileId,                source: 'api-copy',                copyFrom: attachmentId,                copyStorage: sourceStrategy.getStorageName()              };              // Create new attachment              const uploader = Attachments.insert({                file: file,                meta: meta,                isBase64: false,                transport: 'http'              });              if (uploader) {                sendJsonResponse(res, 200, {                  success: true,                  sourceAttachmentId: attachmentId,                  newAttachmentId: uploader._id,                  fileName: sourceAttachment.name,                  fileSize: sourceAttachment.size,                  message: 'Attachment copied successfully'                });              } else {                sendErrorResponse(res, 500, 'Failed to copy attachment');              }            } catch (error) {              sendErrorResponse(res, 500, error.message);            }          });          readStream.on('error', (error) => {            sendErrorResponse(res, 500, error.message);          });        } catch (error) {          console.error('API attachment copy error:', error);          sendErrorResponse(res, 500, error.message);        }      });    } catch (error) {      sendErrorResponse(res, 401, error.message);    }  });  // Move attachment endpoint  WebApp.connectHandlers.use('/api/attachment/move', (req, res, next) => {    if (req.method !== 'POST') {      return next();    }    try {      const userId = authenticateApiRequest(req);            let body = '';      req.on('data', chunk => {        body += chunk.toString();      });      req.on('end', () => {        try {          const data = JSON.parse(body);          const { attachmentId, targetBoardId, targetSwimlaneId, targetListId, targetCardId } = data;          // Get source attachment          const sourceAttachment = ReactiveCache.getAttachment(attachmentId);          if (!sourceAttachment) {            return sendErrorResponse(res, 404, 'Source attachment not found');          }          // Check source permissions          const sourceBoard = ReactiveCache.getBoard(sourceAttachment.meta.boardId);          if (!sourceBoard || !sourceBoard.isBoardMember(userId)) {            return sendErrorResponse(res, 403, 'You do not have permission to access the source attachment');          }          // Check target permissions          const targetBoard = ReactiveCache.getBoard(targetBoardId);          if (!targetBoard || !targetBoard.isBoardMember(userId)) {            return sendErrorResponse(res, 403, 'You do not have permission to modify the target card');          }          // Check if target board allows attachments          if (!targetBoard.allowsAttachments) {            return sendErrorResponse(res, 403, 'Attachments are not allowed on the target board');          }          // Update attachment metadata          Attachments.update(attachmentId, {            $set: {              'meta.boardId': targetBoardId,              'meta.swimlaneId': targetSwimlaneId,              'meta.listId': targetListId,              'meta.cardId': targetCardId,              'meta.source': 'api-move',              'meta.movedAt': new Date()            }          });          sendJsonResponse(res, 200, {            success: true,            attachmentId: attachmentId,            fileName: sourceAttachment.name,            fileSize: sourceAttachment.size,            sourceBoardId: sourceAttachment.meta.boardId,            targetBoardId: targetBoardId,            message: 'Attachment moved successfully'          });        } catch (error) {          console.error('API attachment move error:', error);          sendErrorResponse(res, 500, error.message);        }      });    } catch (error) {      sendErrorResponse(res, 401, error.message);    }  });  // Delete attachment endpoint  WebApp.connectHandlers.use('/api/attachment/delete/([^/]+)', (req, res, next) => {    if (req.method !== 'DELETE') {      return next();    }    try {      const userId = authenticateApiRequest(req);      const attachmentId = req.params[0];      // Get attachment      const attachment = ReactiveCache.getAttachment(attachmentId);      if (!attachment) {        return sendErrorResponse(res, 404, 'Attachment not found');      }      // Check permissions      const board = ReactiveCache.getBoard(attachment.meta.boardId);      if (!board || !board.isBoardMember(userId)) {        return sendErrorResponse(res, 403, 'You do not have permission to delete this attachment');      }      // Delete attachment      Attachments.remove(attachmentId);      sendJsonResponse(res, 200, {        success: true,        attachmentId: attachmentId,        fileName: attachment.name,        message: 'Attachment deleted successfully'      });    } catch (error) {      sendErrorResponse(res, 401, error.message);    }  });  // Get attachment info endpoint  WebApp.connectHandlers.use('/api/attachment/info/([^/]+)', (req, res, next) => {    if (req.method !== 'GET') {      return next();    }    try {      const userId = authenticateApiRequest(req);      const attachmentId = req.params[0];      // Get attachment      const attachment = ReactiveCache.getAttachment(attachmentId);      if (!attachment) {        return sendErrorResponse(res, 404, 'Attachment not found');      }      // Check permissions      const board = ReactiveCache.getBoard(attachment.meta.boardId);      if (!board || !board.isBoardMember(userId)) {        return sendErrorResponse(res, 403, 'You do not have permission to access this attachment');      }      const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');            sendJsonResponse(res, 200, {        success: true,        attachmentId: attachment._id,        fileName: attachment.name,        fileSize: attachment.size,        fileType: attachment.type,        storageBackend: strategy.getStorageName(),        boardId: attachment.meta.boardId,        swimlaneId: attachment.meta.swimlaneId,        listId: attachment.meta.listId,        cardId: attachment.meta.cardId,        createdAt: attachment.uploadedAt,        isImage: attachment.isImage,        versions: Object.keys(attachment.versions).map(versionName => ({          versionName: versionName,          storage: attachment.versions[versionName].storage,          size: attachment.versions[versionName].size,          type: attachment.versions[versionName].type        }))      });    } catch (error) {      sendErrorResponse(res, 401, error.message);    }  });}
 |