feat: private certificates/documents buckets with SeaweedFS support
Add Document CRUD, temp→bucket commit flow, SeaweedFS env aliases, and default temp bucket name to temp.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
// /components/documents/documentController.js
|
||||
|
||||
const catchAsync = require('../../utils/catchAsync');
|
||||
const documentService = require('./documentService');
|
||||
const { successResponse, listResponse } = require('../../utils/apiResponse');
|
||||
|
||||
exports.create = catchAsync(async (req, res) => {
|
||||
const document = await documentService.createDocument(req.body, req.user?._id);
|
||||
return successResponse(res, 201, 'Document created successfully', document);
|
||||
});
|
||||
|
||||
exports.getOne = catchAsync(async (req, res) => {
|
||||
const document = await documentService.getDocumentById(req.params.id);
|
||||
return successResponse(res, 200, 'Document retrieved successfully', document);
|
||||
});
|
||||
|
||||
exports.getAll = catchAsync(async (req, res) => {
|
||||
const { data, meta } = await documentService.getAllDocuments(req.query);
|
||||
return listResponse(res, 200, data, meta);
|
||||
});
|
||||
|
||||
exports.getByUser = catchAsync(async (req, res) => {
|
||||
const data = await documentService.getDocumentsByUser(req.params.userId);
|
||||
return successResponse(res, 200, 'User documents retrieved successfully', data);
|
||||
});
|
||||
|
||||
exports.update = catchAsync(async (req, res) => {
|
||||
const document = await documentService.updateDocument(req.params.id, req.body);
|
||||
return successResponse(res, 200, 'Document updated successfully', document);
|
||||
});
|
||||
|
||||
exports.delete = catchAsync(async (req, res) => {
|
||||
await documentService.deleteDocument(req.params.id);
|
||||
return successResponse(res, 200, 'Document deleted successfully');
|
||||
});
|
||||
|
||||
exports.getMyDocuments = catchAsync(async (req, res) => {
|
||||
const { data, meta } = await documentService.getMyDocuments(req.user._id, req.query);
|
||||
return listResponse(res, 200, data, meta);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
// /components/documents/documentModel.js
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const documentSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
/** Permanent object key inside the documents bucket */
|
||||
fileKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
/** Original uploaded filename */
|
||||
fileName: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
/** Public URL only when the documents bucket is public; otherwise null */
|
||||
fileUrl: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
bucket: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: 'documents'
|
||||
},
|
||||
mimeType: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
uploadedBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Document', documentSchema);
|
||||
@@ -0,0 +1,22 @@
|
||||
// /components/documents/documentRoutes.js
|
||||
|
||||
const express = require('express');
|
||||
const documentController = require('./documentController');
|
||||
const authMiddleware = require('../../middlewares/authMiddleware');
|
||||
const perm = require('../../middlewares/permissionMiddleware');
|
||||
const { PERMISSIONS } = require('../../constants/permissions');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/user/my-documents', perm.requires(PERMISSIONS.DOCUMENTS_READ), documentController.getMyDocuments);
|
||||
|
||||
router.post('/admin/create', perm.requires(PERMISSIONS.DOCUMENTS_CREATE), documentController.create);
|
||||
router.get('/admin/get-all', perm.requires(PERMISSIONS.DOCUMENTS_READ), documentController.getAll);
|
||||
router.get('/admin/by-user/:userId', perm.requires(PERMISSIONS.DOCUMENTS_READ), documentController.getByUser);
|
||||
router.get('/admin/get-one/:id', perm.requires(PERMISSIONS.DOCUMENTS_READ), documentController.getOne);
|
||||
router.put('/admin/update/:id', perm.requires(PERMISSIONS.DOCUMENTS_UPDATE), documentController.update);
|
||||
router.delete('/admin/delete/:id', perm.requires(PERMISSIONS.DOCUMENTS_DELETE), documentController.delete);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,140 @@
|
||||
// /components/documents/documentService.js
|
||||
|
||||
const path = require('path');
|
||||
const Document = require('./documentModel');
|
||||
const User = require('../users/userModel');
|
||||
const AppError = require('../../utils/AppError');
|
||||
const config = require('../../config/config');
|
||||
const {
|
||||
commitTempFile,
|
||||
generatePresignedUrl,
|
||||
deleteFromBucket
|
||||
} = require('../../utils/s3Client');
|
||||
const { parsePaginationAndSort, buildFilterQuery, calculateMeta } = require('../../utils/pagination');
|
||||
|
||||
const BUCKET = 'documents';
|
||||
|
||||
const withAccessUrl = async (doc) => {
|
||||
const item = typeof doc.toObject === 'function' ? doc.toObject() : { ...doc };
|
||||
item.fileUrl = item.fileUrl || null;
|
||||
item.signedUrl = await generatePresignedUrl(item.fileKey, item.bucket || config.S3_DOCUMENTS_BUCKET);
|
||||
item.presignedUrl = item.signedUrl;
|
||||
item.url = item.fileUrl || item.signedUrl;
|
||||
return item;
|
||||
};
|
||||
|
||||
const createDocument = async (data, uploadedBy = null) => {
|
||||
const user = await User.findById(data.user);
|
||||
if (!user) throw new AppError('USER_NOT_FOUND');
|
||||
if (!data.tempFileName) throw new AppError('FILE_REQUIRED');
|
||||
|
||||
const safeName = path.basename(data.tempFileName);
|
||||
const targetKey = `documents/doc-${Date.now()}-${safeName}`;
|
||||
const { fileKey, bucket, fileUrl } = await commitTempFile(data.tempFileName, targetKey, BUCKET);
|
||||
|
||||
const document = await Document.create({
|
||||
user: data.user,
|
||||
title: data.title || data.originalName || safeName,
|
||||
description: data.description || '',
|
||||
fileKey,
|
||||
fileName: data.originalName || data.fileName || safeName,
|
||||
fileUrl: fileUrl || null,
|
||||
bucket,
|
||||
mimeType: data.mimeType || '',
|
||||
uploadedBy: uploadedBy || null
|
||||
});
|
||||
|
||||
if (Array.isArray(user.documents)) {
|
||||
user.documents.push(document._id);
|
||||
await user.save();
|
||||
}
|
||||
|
||||
return withAccessUrl(document);
|
||||
};
|
||||
|
||||
const getDocumentById = async (id) => {
|
||||
const document = await Document.findById(id).populate('user', 'name username nationalIdCode');
|
||||
if (!document) throw new AppError('DOCUMENT_NOT_FOUND');
|
||||
return withAccessUrl(document);
|
||||
};
|
||||
|
||||
const getDocumentsByUser = async (userId) => {
|
||||
const documents = await Document.find({ user: userId }).sort({ createdAt: -1 });
|
||||
return Promise.all(documents.map(withAccessUrl));
|
||||
};
|
||||
|
||||
const getAllDocuments = async (queryParams) => {
|
||||
const { page, limit, skip, sort } = parsePaginationAndSort(queryParams);
|
||||
const filter = buildFilterQuery(queryParams, ['title', 'fileName', 'description']);
|
||||
if (queryParams.userId) filter.user = queryParams.userId;
|
||||
|
||||
const [documents, totalCount] = await Promise.all([
|
||||
Document.find(filter)
|
||||
.populate('user', 'name username')
|
||||
.sort(sort)
|
||||
.skip(skip)
|
||||
.limit(limit),
|
||||
Document.countDocuments(filter)
|
||||
]);
|
||||
|
||||
const data = await Promise.all(documents.map(withAccessUrl));
|
||||
return { data, meta: calculateMeta(totalCount, page, limit) };
|
||||
};
|
||||
|
||||
const updateDocument = async (id, updateData) => {
|
||||
const document = await Document.findById(id);
|
||||
if (!document) throw new AppError('DOCUMENT_NOT_FOUND');
|
||||
|
||||
if (updateData.tempFileName) {
|
||||
const safeName = path.basename(updateData.tempFileName);
|
||||
const targetKey = `documents/doc-${Date.now()}-${safeName}`;
|
||||
const { fileKey, bucket, fileUrl } = await commitTempFile(updateData.tempFileName, targetKey, BUCKET);
|
||||
await deleteFromBucket(document.fileKey, document.bucket || config.S3_DOCUMENTS_BUCKET);
|
||||
document.fileKey = fileKey;
|
||||
document.bucket = bucket;
|
||||
document.fileUrl = fileUrl || null;
|
||||
if (updateData.originalName || updateData.fileName) {
|
||||
document.fileName = updateData.originalName || updateData.fileName;
|
||||
}
|
||||
if (updateData.mimeType) document.mimeType = updateData.mimeType;
|
||||
}
|
||||
|
||||
if (updateData.title) document.title = updateData.title;
|
||||
if (updateData.description !== undefined) document.description = updateData.description;
|
||||
|
||||
await document.save();
|
||||
return withAccessUrl(document);
|
||||
};
|
||||
|
||||
const deleteDocument = async (id) => {
|
||||
const document = await Document.findById(id);
|
||||
if (!document) throw new AppError('DOCUMENT_NOT_FOUND');
|
||||
|
||||
await deleteFromBucket(document.fileKey, document.bucket || config.S3_DOCUMENTS_BUCKET);
|
||||
await User.findByIdAndUpdate(document.user, { $pull: { documents: document._id } });
|
||||
await Document.findByIdAndDelete(id);
|
||||
return null;
|
||||
};
|
||||
|
||||
const getMyDocuments = async (userId, queryParams = {}) => {
|
||||
const filter = { user: userId, ...buildFilterQuery(queryParams, ['title', 'fileName']) };
|
||||
const { page, limit, skip, sort } = parsePaginationAndSort(queryParams);
|
||||
|
||||
const [documents, totalCount] = await Promise.all([
|
||||
Document.find(filter).sort(sort).skip(skip).limit(limit),
|
||||
Document.countDocuments(filter)
|
||||
]);
|
||||
|
||||
const data = await Promise.all(documents.map(withAccessUrl));
|
||||
return { data, meta: calculateMeta(totalCount, page, limit) };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createDocument,
|
||||
getDocumentById,
|
||||
getDocumentsByUser,
|
||||
getAllDocuments,
|
||||
updateDocument,
|
||||
deleteDocument,
|
||||
getMyDocuments
|
||||
};
|
||||
Reference in New Issue
Block a user