Implementation de la feature de gestion des fichiers sur les investissements
This commit is contained in:
1 parent
f6829dec9d
commit
8953d0bb35
21 files changed
+1729
-181
No files matched your search
@@ -14,6 +14,7 @@ import { runAutoExport } from '../jobs/autoExport.js';
|
||||
import { audit } from '../utils/audit.js';
|
||||
import multer from 'multer';
|
||||
import { createZip, readZip } from '../utils/zip.js';
|
||||
import { deleteAllDocumentFilesForUser, buildAllDocumentsZipEntries } from './documents.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const dataDir = process.env.DATA_DIR
|
||||
@@ -64,10 +65,21 @@ const router = Router();
|
||||
|
||||
/* ── Utilisateurs ─────────────────────────────────────────────────────── */
|
||||
|
||||
/** Liste tous les utilisateurs */
|
||||
/** Liste tous les utilisateurs, avec quelques compteurs d'usage par compte
|
||||
* (plateformes, investissements, documents) — demande Olivier 29/08/26,
|
||||
* colonnes affichées dans Administration > Utilisateurs. Sous-requêtes
|
||||
* corrélées plutôt que JOIN + GROUP BY : chaque compte reste une ligne
|
||||
* distincte sans risquer de doublons liés aux jointures multiples. */
|
||||
router.get('/users', (req, res) => {
|
||||
const users = db.prepare(`
|
||||
SELECT id, email, display_name, role, email_verified, totp_enabled, status, created_at
|
||||
SELECT
|
||||
id, email, display_name, role, email_verified, totp_enabled, status, created_at,
|
||||
(SELECT COUNT(*) FROM plateformes p WHERE p.user_id = users.id) AS nb_plateformes,
|
||||
(SELECT COUNT(*) FROM investissements i
|
||||
JOIN investisseurs inv ON inv.id = i.investisseur_id
|
||||
WHERE inv.user_id = users.id) AS nb_investissements,
|
||||
(SELECT COUNT(*) FROM documents d WHERE d.user_id = users.id) AS nb_documents,
|
||||
(SELECT COALESCE(SUM(d.taille_octets), 0) FROM documents d WHERE d.user_id = users.id) AS taille_documents
|
||||
FROM users
|
||||
ORDER BY id ASC
|
||||
`).all();
|
||||
@@ -176,6 +188,14 @@ router.delete('/users/:id', (req, res, next) => {
|
||||
throw new HttpError(400, 'Vous ne pouvez pas supprimer votre propre compte');
|
||||
}
|
||||
const targetUser = db.prepare('SELECT email, display_name FROM users WHERE id = ?').get(targetId);
|
||||
if (!targetUser) throw new HttpError(404, 'Utilisateur introuvable');
|
||||
|
||||
// Fichiers documents de l'utilisateur : à effacer AVANT le DELETE FROM users
|
||||
// (les lignes `documents` seront supprimées en cascade par la contrainte
|
||||
// ON DELETE CASCADE, mais une cascade SQLite n'efface jamais les fichiers
|
||||
// du disque — il faut les lister pendant que le user existe encore).
|
||||
deleteAllDocumentFilesForUser(targetId);
|
||||
|
||||
const r = db.prepare('DELETE FROM users WHERE id = ?').run(targetId);
|
||||
if (r.changes === 0) throw new HttpError(404, 'Utilisateur introuvable');
|
||||
audit(req, { action: 'user_deleted', category: 'account', actorId: req.user.id, details: { email: targetUser?.email, display_name: targetUser?.display_name } });
|
||||
@@ -497,6 +517,11 @@ router.get('/export-full', (req, res, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Fichiers documents (tous utilisateurs) — la base SQLite ci-dessus contient
|
||||
// déjà les lignes `documents`, mais pas les fichiers physiques qu'elles
|
||||
// référencent ; cf. demande Olivier 29/08/26.
|
||||
entries.push(...buildAllDocumentsZipEntries());
|
||||
|
||||
const zipBuf = createZip(entries);
|
||||
|
||||
// Sauvegarde sur disque + purge
|
||||
@@ -597,7 +622,7 @@ router.delete('/exports/:filename', (req, res, next) => {
|
||||
* POST /api/admin/exports/:filename/restore
|
||||
* Restaure l'environnement depuis un export stocké :
|
||||
* 1. Sauvegarde l'état courant dans exports/ (filet de sécurité)
|
||||
* 2. Copie les assets (logos, icons) immédiatement
|
||||
* 2. Copie les assets (logos, icons, documents) immédiatement
|
||||
* 3. Écrit la nouvelle DB dans {DB_PATH}.pending-restore
|
||||
* 4. Répond au client, puis redémarre le processus (process.exit)
|
||||
* → En prod (DATA_DIR défini), le restart policy Docker relance le conteneur
|
||||
@@ -642,7 +667,10 @@ router.post('/exports/:filename/restore', async (req, res, next) => {
|
||||
}, null, 2),
|
||||
});
|
||||
backupEntries.push({ name: 'crowdlending.db', data: fs.readFileSync(tmpDb) });
|
||||
for (const subdir of ['logos', 'icons']) {
|
||||
// 'documents' inclus au même titre que logos/icons : c'est un filet de
|
||||
// sécurité, on sauvegarde tout ce qui est actuellement sur disque avant
|
||||
// de l'écraser à l'étape 2, indépendamment de ce que référence la base.
|
||||
for (const subdir of ['logos', 'icons', 'documents']) {
|
||||
const dir = path.join(dataDir, subdir);
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
for (const f of fs.readdirSync(dir)) {
|
||||
@@ -657,8 +685,16 @@ router.post('/exports/:filename/restore', async (req, res, next) => {
|
||||
fs.writeFileSync(path.join(exportsDir, `pre-restore-backup-${ts}.zip`), createZip(backupEntries));
|
||||
purgeOldExports();
|
||||
|
||||
// 2. Remplacement des assets (logos + icons) — safe à faire en live
|
||||
for (const subdir of ['logos', 'icons']) {
|
||||
// 2. Remplacement des assets (logos + icons + documents) — safe à faire en live.
|
||||
// 'documents' suit exactement le même traitement que logos/icons (demande
|
||||
// Olivier 29/08/26) : le dossier est vidé puis repeuplé depuis le zip.
|
||||
// Même remarque que pour logos/icons : entre ce remplacement et le
|
||||
// redémarrage effectif du process qui applique le pending-restore de la
|
||||
// base (étape 3/4), la base encore active référence des `filename` qui
|
||||
// viennent d'être remplacés par ceux de l'export importé — fenêtre de
|
||||
// cohérence transitoire déjà acceptée pour logos/icons, désormais partagée
|
||||
// par documents.
|
||||
for (const subdir of ['logos', 'icons', 'documents']) {
|
||||
const dir = path.join(dataDir, subdir);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
// Vidage du dossier existant (fichiers et sous-dossiers)
|
||||
|
||||
Reference in new issue
Block a user