CentralBackend/services/dev/DevManagementService.js

349 lines
9.5 KiB
JavaScript

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();
try {
const [rows, fields] = await conn.query(sql);
return { rows, fields };
} finally {
try { await conn.end(); } catch (_) {}
}
}
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,
contractCategory: target.contractCategory,
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,
contractCategory: target.contractCategory,
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,
listFolderStructureIssues,
createFolderStructure,
listLooseFiles,
moveLooseFilesToContract,
listGhostDirectories
};