feat: implement Dev Management features for folder structure and loose file handling and ghost dirs
This commit is contained in:
parent
e2a6b215d5
commit
295bb85536
@ -32,3 +32,55 @@ exports.importSqlDump = async (req, res) => {
|
||||
return res.status(500).json({ success: false, error: e?.message || 'SQL execution failed' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.listFolderStructureIssues = async (_req, res) => {
|
||||
try {
|
||||
const { list, meta } = await DevManagementService.listFolderStructureIssues();
|
||||
return res.json({ success: true, data: list, meta });
|
||||
} catch (e) {
|
||||
logger.error('[DevManagementController.listFolderStructureIssues] error', { msg: e?.message });
|
||||
return res.status(500).json({ success: false, error: e?.message || 'Failed to list folder structure issues' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.createFolderStructure = async (req, res) => {
|
||||
try {
|
||||
const userId = req.body && (req.body.userId || req.body.user_id);
|
||||
const { results, meta } = await DevManagementService.createFolderStructure({ userId });
|
||||
return res.json({ success: true, data: results, meta });
|
||||
} catch (e) {
|
||||
logger.error('[DevManagementController.createFolderStructure] error', { msg: e?.message });
|
||||
return res.status(500).json({ success: false, error: e?.message || 'Failed to create folder structure' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.listLooseFiles = async (_req, res) => {
|
||||
try {
|
||||
const { list, meta } = await DevManagementService.listLooseFiles();
|
||||
return res.json({ success: true, data: list, meta });
|
||||
} catch (e) {
|
||||
logger.error('[DevManagementController.listLooseFiles] error', { msg: e?.message });
|
||||
return res.status(500).json({ success: false, error: e?.message || 'Failed to list loose files' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.moveLooseFilesToContract = async (req, res) => {
|
||||
try {
|
||||
const userId = req.body && (req.body.userId || req.body.user_id);
|
||||
const { results, meta } = await DevManagementService.moveLooseFilesToContract({ userId });
|
||||
return res.json({ success: true, data: results, meta });
|
||||
} catch (e) {
|
||||
logger.error('[DevManagementController.moveLooseFilesToContract] error', { msg: e?.message });
|
||||
return res.status(500).json({ success: false, error: e?.message || 'Failed to move loose files' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.listGhostDirectories = async (_req, res) => {
|
||||
try {
|
||||
const { list, meta } = await DevManagementService.listGhostDirectories();
|
||||
return res.json({ success: true, data: list, meta });
|
||||
} catch (e) {
|
||||
logger.error('[DevManagementController.listGhostDirectories] error', { msg: e?.message });
|
||||
return res.status(500).json({ success: false, error: e?.message || 'Failed to list ghost directories' });
|
||||
}
|
||||
};
|
||||
|
||||
@ -26,6 +26,7 @@ const AffiliateController = require('../controller/affiliate/AffiliateController
|
||||
const AbonemmentController = require('../controller/abonemments/AbonemmentController');
|
||||
const NewsController = require('../controller/news/NewsController');
|
||||
const InvoiceController = require('../controller/invoice/InvoiceController'); // NEW
|
||||
const DevManagementController = require('../controller/dev/DevManagementController');
|
||||
|
||||
// small helpers copied from original files
|
||||
|
||||
@ -58,6 +59,9 @@ router.get('/admin/verification-pending-users', authMiddleware, adminOnly, Admin
|
||||
router.get('/admin/unverified-users', authMiddleware, adminOnly, AdminUserController.getUnverifiedUsers);
|
||||
router.get('/admin/user/:id/documents', authMiddleware, adminOnly, UserDocumentController.getAllDocumentsForUser);
|
||||
router.get('/admin/server-status', authMiddleware, adminOnly, ServerStatusController.getStatus);
|
||||
router.get('/admin/dev/exoscale/folder-structure-issues', authMiddleware, adminOnly, DevManagementController.listFolderStructureIssues);
|
||||
router.get('/admin/dev/exoscale/loose-files', authMiddleware, adminOnly, DevManagementController.listLooseFiles);
|
||||
router.get('/admin/dev/exoscale/ghost-directories', authMiddleware, adminOnly, DevManagementController.listGhostDirectories);
|
||||
// Contract preview for admin: latest active by user type
|
||||
router.get('/admin/contracts/:id/preview', authMiddleware, adminOnly, DocumentTemplateController.previewLatestForUser);
|
||||
|
||||
|
||||
@ -162,6 +162,9 @@ router.post('/admin/news', authMiddleware, adminOnly, upload.single('image'), Ne
|
||||
|
||||
// NEW: Dev Management SQL dump import (admin + super_admin)
|
||||
router.post('/admin/dev/sql', authMiddleware, adminOnly, upload.single('file'), DevManagementController.importSqlDump);
|
||||
// NEW: Dev Management Exoscale folder structure + loose file actions (admin + super_admin)
|
||||
router.post('/admin/dev/exoscale/create-folder-structure', authMiddleware, adminOnly, DevManagementController.createFolderStructure);
|
||||
router.post('/admin/dev/exoscale/move-loose-files', authMiddleware, adminOnly, DevManagementController.moveLooseFilesToContract);
|
||||
|
||||
// Abonement POSTs
|
||||
router.post('/abonements/subscribe', authMiddleware, AbonemmentController.subscribe);
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
const db = require('../../database/database');
|
||||
const AdminRepository = require('../../repositories/admin/AdminRepository');
|
||||
const { s3 } = require('../../utils/exoscaleUploader');
|
||||
const { ListObjectsV2Command, CopyObjectCommand, DeleteObjectCommand, PutObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const { logger } = require('../../middleware/logger');
|
||||
const path = require('path');
|
||||
|
||||
async function executeDump(sql) {
|
||||
const conn = await db.getMultiStatementConnection();
|
||||
@ -10,6 +15,332 @@ async function executeDump(sql) {
|
||||
}
|
||||
}
|
||||
|
||||
async function listAllKeys(prefix) {
|
||||
const keys = [];
|
||||
let token = undefined;
|
||||
do {
|
||||
const res = await s3.send(new ListObjectsV2Command({
|
||||
Bucket: process.env.EXOSCALE_BUCKET,
|
||||
Prefix: prefix,
|
||||
ContinuationToken: token
|
||||
}));
|
||||
const batch = (res && res.Contents ? res.Contents : [])
|
||||
.map(item => item && item.Key)
|
||||
.filter(Boolean);
|
||||
keys.push(...batch);
|
||||
token = res && res.IsTruncated ? res.NextContinuationToken : undefined;
|
||||
} while (token);
|
||||
return keys;
|
||||
}
|
||||
|
||||
async function listTopLevelFolders(prefix) {
|
||||
const folders = [];
|
||||
let token = undefined;
|
||||
do {
|
||||
const res = await s3.send(new ListObjectsV2Command({
|
||||
Bucket: process.env.EXOSCALE_BUCKET,
|
||||
Prefix: prefix,
|
||||
Delimiter: '/',
|
||||
ContinuationToken: token
|
||||
}));
|
||||
const batch = (res && res.CommonPrefixes ? res.CommonPrefixes : [])
|
||||
.map(item => item && item.Prefix)
|
||||
.filter(Boolean);
|
||||
folders.push(...batch);
|
||||
token = res && res.IsTruncated ? res.NextContinuationToken : undefined;
|
||||
} while (token);
|
||||
return folders;
|
||||
}
|
||||
|
||||
function getTimestamp() {
|
||||
const d = new Date();
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
async function objectExists(key) {
|
||||
try {
|
||||
await s3.send(new HeadObjectCommand({
|
||||
Bucket: process.env.EXOSCALE_BUCKET,
|
||||
Key: key
|
||||
}));
|
||||
return true;
|
||||
} catch (e) {
|
||||
const status = e && (e.$metadata && e.$metadata.httpStatusCode);
|
||||
if (status === 404) return false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function appendTimestampToKey(key) {
|
||||
const ext = path.posix.extname(key);
|
||||
const base = ext ? key.slice(0, -ext.length) : key;
|
||||
return `${base}-${getTimestamp()}${ext}`;
|
||||
}
|
||||
|
||||
async function getUsersForScan() {
|
||||
const conn = await db.getConnection();
|
||||
try {
|
||||
return await AdminRepository.getUserList(conn);
|
||||
} finally {
|
||||
try { conn.release(); } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function getDisplayName(user) {
|
||||
if (!user) return null;
|
||||
if ((user.user_type || '').toLowerCase() === 'company') return user.company_name || null;
|
||||
const first = user.first_name || '';
|
||||
const last = user.last_name || '';
|
||||
const full = `${first} ${last}`.trim();
|
||||
return full || null;
|
||||
}
|
||||
|
||||
async function listFolderStructureIssues() {
|
||||
const users = await getUsersForScan();
|
||||
const results = [];
|
||||
let scannedUsers = 0;
|
||||
|
||||
for (const user of users) {
|
||||
const userType = (user.user_type || '').toLowerCase();
|
||||
if (!['personal', 'company'].includes(userType)) continue;
|
||||
scannedUsers += 1;
|
||||
|
||||
const contractCategory = userType === 'company' ? 'company' : 'personal';
|
||||
const basePrefix = `contracts/${contractCategory}/${user.id}/`;
|
||||
const keys = await listAllKeys(basePrefix);
|
||||
if (!keys.length) continue;
|
||||
|
||||
const hasContractFolder = keys.some(k => k.startsWith(`${basePrefix}contract/`));
|
||||
const hasGdprFolder = keys.some(k => k.startsWith(`${basePrefix}gdpr/`));
|
||||
if (hasContractFolder && hasGdprFolder) continue;
|
||||
|
||||
results.push({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
userType,
|
||||
name: getDisplayName(user),
|
||||
contractCategory,
|
||||
basePrefix,
|
||||
totalObjects: keys.length,
|
||||
hasContractFolder,
|
||||
hasGdprFolder
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
list: results,
|
||||
meta: {
|
||||
scannedUsers,
|
||||
invalidCount: results.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function createFolderStructure({ userId } = {}) {
|
||||
const { list } = await listFolderStructureIssues();
|
||||
const targetId = userId ? Number(userId) : null;
|
||||
const targets = targetId ? list.filter(u => u.userId === targetId) : list;
|
||||
const results = [];
|
||||
let createdTotal = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const target of targets) {
|
||||
const prefix = target.basePrefix;
|
||||
const keys = await listAllKeys(prefix);
|
||||
const hasContractFolder = keys.some(k => k.startsWith(`${prefix}contract/`));
|
||||
const hasGdprFolder = keys.some(k => k.startsWith(`${prefix}gdpr/`));
|
||||
|
||||
let created = 0;
|
||||
const errors = [];
|
||||
|
||||
const ensureFolder = async (folderKey) => {
|
||||
try {
|
||||
await s3.send(new PutObjectCommand({
|
||||
Bucket: process.env.EXOSCALE_BUCKET,
|
||||
Key: folderKey,
|
||||
Body: ''
|
||||
}));
|
||||
created += 1;
|
||||
} catch (e) {
|
||||
errorCount += 1;
|
||||
const message = e && (e.message || e.toString());
|
||||
logger.error('DevManagementService.createFolderStructure:create_failed', { folderKey, error: message });
|
||||
errors.push({ folderKey, message });
|
||||
}
|
||||
};
|
||||
|
||||
if (!hasContractFolder) await ensureFolder(`${prefix}contract/`);
|
||||
if (!hasGdprFolder) await ensureFolder(`${prefix}gdpr/`);
|
||||
|
||||
createdTotal += created;
|
||||
results.push({
|
||||
userId: target.userId,
|
||||
created,
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
results,
|
||||
meta: {
|
||||
processedUsers: targets.length,
|
||||
createdTotal,
|
||||
errorCount
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function listLooseFiles() {
|
||||
const users = await getUsersForScan();
|
||||
const results = [];
|
||||
let scannedUsers = 0;
|
||||
|
||||
for (const user of users) {
|
||||
const userType = (user.user_type || '').toLowerCase();
|
||||
if (!['personal', 'company'].includes(userType)) continue;
|
||||
scannedUsers += 1;
|
||||
|
||||
const contractCategory = userType === 'company' ? 'company' : 'personal';
|
||||
const basePrefix = `contracts/${contractCategory}/${user.id}/`;
|
||||
const keys = await listAllKeys(basePrefix);
|
||||
if (!keys.length) continue;
|
||||
|
||||
const looseKeys = keys
|
||||
.filter(k => !!k)
|
||||
.filter(k => !k.endsWith('/'))
|
||||
.filter(k => !k.startsWith(`${basePrefix}contract/`))
|
||||
.filter(k => !k.startsWith(`${basePrefix}gdpr/`));
|
||||
|
||||
if (!looseKeys.length) continue;
|
||||
|
||||
results.push({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
userType,
|
||||
name: getDisplayName(user),
|
||||
contractCategory,
|
||||
basePrefix,
|
||||
looseObjects: looseKeys.length,
|
||||
sampleKeys: looseKeys.slice(0, 5)
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
list: results,
|
||||
meta: {
|
||||
scannedUsers,
|
||||
looseCount: results.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function moveLooseFilesToContract({ userId } = {}) {
|
||||
const { list } = await listLooseFiles();
|
||||
const targetId = userId ? Number(userId) : null;
|
||||
const targets = targetId ? list.filter(u => u.userId === targetId) : list;
|
||||
const results = [];
|
||||
let movedTotal = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const target of targets) {
|
||||
const prefix = target.basePrefix;
|
||||
const keys = await listAllKeys(prefix);
|
||||
const toMove = keys
|
||||
.filter(k => !!k)
|
||||
.filter(k => !k.endsWith('/'))
|
||||
.filter(k => !k.startsWith(`${prefix}contract/`))
|
||||
.filter(k => !k.startsWith(`${prefix}gdpr/`));
|
||||
|
||||
let moved = 0;
|
||||
let skipped = 0;
|
||||
const errors = [];
|
||||
|
||||
for (const key of toMove) {
|
||||
const baseName = path.posix.basename(key);
|
||||
let destKey = `${prefix}contract/${baseName}`;
|
||||
if (await objectExists(destKey)) {
|
||||
destKey = appendTimestampToKey(destKey);
|
||||
}
|
||||
if (destKey === key) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const copySource = `${process.env.EXOSCALE_BUCKET}/${encodeURIComponent(key)}`;
|
||||
await s3.send(new CopyObjectCommand({
|
||||
Bucket: process.env.EXOSCALE_BUCKET,
|
||||
CopySource: copySource,
|
||||
Key: destKey
|
||||
}));
|
||||
await s3.send(new DeleteObjectCommand({
|
||||
Bucket: process.env.EXOSCALE_BUCKET,
|
||||
Key: key
|
||||
}));
|
||||
moved += 1;
|
||||
} catch (e) {
|
||||
errorCount += 1;
|
||||
const message = e && (e.message || e.toString());
|
||||
logger.error('DevManagementService.moveLooseFilesToContract:move_failed', { key, destKey, error: message });
|
||||
errors.push({ key, destKey, message });
|
||||
}
|
||||
}
|
||||
|
||||
movedTotal += moved;
|
||||
results.push({
|
||||
userId: target.userId,
|
||||
moved,
|
||||
skipped,
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
results,
|
||||
meta: {
|
||||
processedUsers: targets.length,
|
||||
movedTotal,
|
||||
errorCount
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function listGhostDirectories() {
|
||||
const users = await getUsersForScan();
|
||||
const userIds = new Set(users.map(u => Number(u.id)).filter(n => Number.isFinite(n)));
|
||||
const results = [];
|
||||
|
||||
const categories = ['personal', 'company'];
|
||||
for (const category of categories) {
|
||||
const base = `contracts/${category}/`;
|
||||
const prefixes = await listTopLevelFolders(base);
|
||||
for (const prefix of prefixes) {
|
||||
const idPart = prefix.replace(base, '').replace(/\/$/, '');
|
||||
if (!/^[0-9]+$/.test(idPart)) continue;
|
||||
const uid = Number(idPart);
|
||||
if (!userIds.has(uid)) {
|
||||
results.push({
|
||||
userId: uid,
|
||||
contractCategory: category,
|
||||
basePrefix: prefix
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
list: results,
|
||||
meta: {
|
||||
ghostCount: results.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
executeDump
|
||||
executeDump,
|
||||
listFolderStructureIssues,
|
||||
createFolderStructure,
|
||||
listLooseFiles,
|
||||
moveLooseFilesToContract,
|
||||
listGhostDirectories
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user