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:
2026-08-15 02:53:59 +03:30
parent 5c2fad4b44
commit 4603b45208
18 changed files with 585 additions and 112 deletions
+57 -72
View File
@@ -4,26 +4,44 @@ const path = require('path');
const Certificate = require('./certificateModel');
const User = require('../users/userModel');
const AppError = require('../../utils/AppError');
const config = require('../../config/config');
const { commitTempFile, generatePresignedUrl, deleteFromBucket } = require('../../utils/s3Client');
const eventEmitter = require('../../events/eventEmitter');
const EVENT_NAMES = require('../../constants/eventNames');
const { parsePaginationAndSort, buildFilterQuery, calculateMeta } = require('../../utils/pagination');
const BUCKET = 'certificates';
const withAccessUrl = async (cert) => {
const item = typeof cert.toObject === 'function' ? cert.toObject() : { ...cert };
item.fileUrl = item.fileUrl || null;
item.signedUrl = await generatePresignedUrl(item.fileKey, item.bucket || config.S3_CERTIFICATES_BUCKET);
item.presignedUrl = item.signedUrl;
item.url = item.fileUrl || item.signedUrl;
return item;
};
const createCertificate = async (data) => {
const user = await User.findById(data.user);
if (!user) throw new AppError('USER_NOT_FOUND');
if (!data.tempFileName) throw new AppError('FILE_REQUIRED');
// Move file from Temp bucket to Permanent Storage bucket
const targetKey = `certificates/cert-${Date.now()}-${path.basename(data.tempFileName)}`;
const { fileKey } = await commitTempFile(data.tempFileName, targetKey);
const safeName = path.basename(data.tempFileName);
const targetKey = `certificates/cert-${Date.now()}-${safeName}`;
const { fileKey, bucket, fileUrl } = await commitTempFile(data.tempFileName, targetKey, BUCKET);
const fileName = data.originalName || data.fileName || safeName;
const certificate = await Certificate.create({
user: data.user,
course: data.course || null,
title: data.title,
title: data.title || fileName,
issuer: data.issuer || '',
fileKey,
originalName: data.originalName || path.basename(data.tempFileName),
fileName,
originalName: fileName,
fileUrl: fileUrl || null,
bucket,
mimeType: data.mimeType || '',
isOfficial: data.isOfficial || false
});
@@ -32,27 +50,28 @@ const createCertificate = async (data) => {
eventEmitter.emit(EVENT_NAMES.CERTIFICATE_ISSUED, { certificateId: certificate._id, userId: user._id });
const resultObj = certificate.toObject();
resultObj.presignedUrl = await generatePresignedUrl(certificate.fileKey);
return resultObj;
return withAccessUrl(certificate);
};
const getCertificateById = async (id) => {
const certificate = await Certificate.findById(id)
.populate('user', 'name username nationalIdCode')
.populate('course', 'title type');
if (!certificate) {
throw new AppError('CERTIFICATE_NOT_FOUND');
}
if (!certificate) throw new AppError('CERTIFICATE_NOT_FOUND');
return withAccessUrl(certificate);
};
const resultObj = certificate.toObject();
resultObj.presignedUrl = await generatePresignedUrl(certificate.fileKey);
return resultObj;
const getCertificatesByUser = async (userId) => {
const certificates = await Certificate.find({ user: userId })
.populate('course', 'title type')
.sort({ createdAt: -1 });
return Promise.all(certificates.map(withAccessUrl));
};
const getAllCertificates = async (queryParams) => {
const { page, limit, skip, sort } = parsePaginationAndSort(queryParams);
const filter = buildFilterQuery(queryParams, ['title', 'issuer']);
if (queryParams.userId) filter.user = queryParams.userId;
const [certificates, totalCount] = await Promise.all([
Certificate.find(filter)
@@ -64,30 +83,28 @@ const getAllCertificates = async (queryParams) => {
Certificate.countDocuments(filter)
]);
const listWithUrls = await Promise.all(
certificates.map(async (cert) => {
const item = cert.toObject();
item.presignedUrl = await generatePresignedUrl(cert.fileKey);
return item;
})
);
const meta = calculateMeta(totalCount, page, limit);
return { data: listWithUrls, meta };
const data = await Promise.all(certificates.map(withAccessUrl));
return { data, meta: calculateMeta(totalCount, page, limit) };
};
const updateCertificate = async (id, updateData) => {
const certificate = await Certificate.findById(id);
if (!certificate) {
throw new AppError('CERTIFICATE_NOT_FOUND');
}
if (!certificate) throw new AppError('CERTIFICATE_NOT_FOUND');
if (updateData.tempFileName) {
const targetKey = `certificates/cert-${Date.now()}-${path.basename(updateData.tempFileName)}`;
const { fileKey } = await commitTempFile(updateData.tempFileName, targetKey);
// Delete old file from storage bucket
await deleteFromBucket(certificate.fileKey);
const safeName = path.basename(updateData.tempFileName);
const targetKey = `certificates/cert-${Date.now()}-${safeName}`;
const { fileKey, bucket, fileUrl } = await commitTempFile(updateData.tempFileName, targetKey, BUCKET);
await deleteFromBucket(certificate.fileKey, certificate.bucket || config.S3_CERTIFICATES_BUCKET);
certificate.fileKey = fileKey;
certificate.bucket = bucket;
certificate.fileUrl = fileUrl || null;
const fileName = updateData.originalName || updateData.fileName;
if (fileName) {
certificate.fileName = fileName;
certificate.originalName = fileName;
}
if (updateData.mimeType) certificate.mimeType = updateData.mimeType;
}
if (updateData.title) certificate.title = updateData.title;
@@ -95,53 +112,28 @@ const updateCertificate = async (id, updateData) => {
if (updateData.isOfficial !== undefined) certificate.isOfficial = updateData.isOfficial;
await certificate.save();
const resultObj = certificate.toObject();
resultObj.presignedUrl = await generatePresignedUrl(certificate.fileKey);
return resultObj;
return withAccessUrl(certificate);
};
const deleteCertificate = async (id) => {
const certificate = await Certificate.findById(id);
if (!certificate) {
throw new AppError('CERTIFICATE_NOT_FOUND');
}
if (!certificate) throw new AppError('CERTIFICATE_NOT_FOUND');
await deleteFromBucket(certificate.fileKey);
await deleteFromBucket(certificate.fileKey, certificate.bucket || config.S3_CERTIFICATES_BUCKET);
await User.findByIdAndUpdate(certificate.user, { $pull: { certificates: certificate._id } });
await Certificate.findByIdAndDelete(id);
return null;
};
const searchCertificates = async (queryParams) => {
return getAllCertificates(queryParams);
};
const searchCertificates = async (queryParams) => getAllCertificates(queryParams);
const uploadUserCertificate = async (userId, data) => {
const user = await User.findById(userId);
if (!user) throw new AppError('USER_NOT_FOUND');
const targetKey = `certificates/cert-${Date.now()}-${path.basename(data.tempFileName)}`;
const { fileKey } = await commitTempFile(data.tempFileName, targetKey);
const certificate = await Certificate.create({
return createCertificate({
...data,
user: userId,
course: data.course || null,
title: data.title,
issuer: data.issuer || 'Self-Uploaded',
fileKey,
originalName: data.originalName || path.basename(data.tempFileName),
isOfficial: false
});
user.certificates.push(certificate._id);
await user.save();
eventEmitter.emit(EVENT_NAMES.CERTIFICATE_UPLOADED, { certificateId: certificate._id, userId });
const resultObj = certificate.toObject();
resultObj.presignedUrl = await generatePresignedUrl(certificate.fileKey);
return resultObj;
};
const getMyCertificates = async (userId, queryParams) => {
@@ -157,21 +149,14 @@ const getMyCertificates = async (userId, queryParams) => {
Certificate.countDocuments(filter)
]);
const listWithUrls = await Promise.all(
certificates.map(async (cert) => {
const item = cert.toObject();
item.presignedUrl = await generatePresignedUrl(cert.fileKey);
return item;
})
);
const meta = calculateMeta(totalCount, page, limit);
return { data: listWithUrls, meta };
const data = await Promise.all(certificates.map(withAccessUrl));
return { data, meta: calculateMeta(totalCount, page, limit) };
};
module.exports = {
createCertificate,
getCertificateById,
getCertificatesByUser,
getAllCertificates,
updateCertificate,
deleteCertificate,