Correctif Contrainte de suppression de compte
This commit is contained in:
@@ -499,6 +499,12 @@ const isBonus = BONUS_VALUES.includes(form.investissement_id);
|
|||||||
- **Règle imposée au prompt** : si le fichier ne permet pas d'identifier avec certitude la plateforme (ou l'investissement pour les remboursements), l'IA doit poser la question à l'utilisateur en ne proposant QUE les noms existants comme réponses possibles, et attendre la réponse avant de générer le JSON — plutôt que de deviner ou d'omettre le champ silencieusement
|
- **Règle imposée au prompt** : si le fichier ne permet pas d'identifier avec certitude la plateforme (ou l'investissement pour les remboursements), l'IA doit poser la question à l'utilisateur en ne proposant QUE les noms existants comme réponses possibles, et attendre la réponse avant de générer le JSON — plutôt que de deviner ou d'omettre le champ silencieusement
|
||||||
- Page restructurée en 3 blocs séquentiels : "1. Contexte de l'import" (sélecteur de module) → "2. Fichier source" → "3. Mappage des colonnes" ; le bloc "Dossier investissement" n'apparaît que si le module actif est `investissements`
|
- Page restructurée en 3 blocs séquentiels : "1. Contexte de l'import" (sélecteur de module) → "2. Fichier source" → "3. Mappage des colonnes" ; le bloc "Dossier investissement" n'apparaît que si le module actif est `investissements`
|
||||||
|
|
||||||
|
### Bug — suppression de compte cassée par plateforme_id ON DELETE RESTRICT
|
||||||
|
- **Symptôme** : `DELETE /api/auth/me` échouait avec `SqliteError: FOREIGN KEY constraint failed` (code `SQLITE_CONSTRAINT_TRIGGER`) à `auth.js:319`.
|
||||||
|
- **Cause** : `depots_retraits.plateforme_id` et `investissements.plateforme_id` étaient en `ON DELETE RESTRICT` (seuls FK RESTRICT du schéma) — lors du `DELETE FROM users`, les branches de cascade `users→plateformes` et `users→investisseurs→depots_retraits/investissements` sont indépendantes ; SQLite peut supprimer la plateforme avant les lignes qui la référencent encore, déclenchant RESTRICT.
|
||||||
|
- **Fix** : migration `fixPlateformeCascade` dans `db/index.js` (recréation table via `__repair_*`, RESTRICT→CASCADE) + `schema.sql` mis à jour + protection déplacée côté application dans `DELETE /api/plateformes/:id` (vérification explicite du nombre d'investissements/dépôts-retraits avant suppression, message clair).
|
||||||
|
- **Règle à retenir** : toute nouvelle table référençant `plateformes`/`investisseurs`/`users` doit être en `CASCADE` (ou `SET NULL`), jamais `RESTRICT` — sinon la suppression de compte se recasse. Les protections anti-suppression-accidentelle doivent être implémentées côté route, pas côté contrainte FK.
|
||||||
|
|
||||||
### Piège outillage — mount bash périmé après édition Edit/Write
|
### Piège outillage — mount bash périmé après édition Edit/Write
|
||||||
- Après une édition via l'outil `Edit`/`Write` (côté fichier réel), une vérification immédiate via `mcp__workspace__bash` (wc/tail/node --check) peut montrer une **version périmée/tronquée** du fichier alors que le fichier réel est complet et correct — le mount bash ne se resynchronise pas instantanément après une écriture Windows-side.
|
- Après une édition via l'outil `Edit`/`Write` (côté fichier réel), une vérification immédiate via `mcp__workspace__bash` (wc/tail/node --check) peut montrer une **version périmée/tronquée** du fichier alors que le fichier réel est complet et correct — le mount bash ne se resynchronise pas instantanément après une écriture Windows-side.
|
||||||
- **Règle** : vérifier l'intégrité d'un fichier édité via l'outil `Read` (relire la queue, vérifier la fermeture propre), pas via bash. N'utiliser bash pour vérifier que si l'écriture a elle-même été faite depuis bash (ex. reconstruction `python3` après troncature confirmée par `Read`).
|
- **Règle** : vérifier l'intégrité d'un fichier édité via l'outil `Read` (relire la queue, vérifier la fermeture propre), pas via bash. N'utiliser bash pour vérifier que si l'écriture a elle-même été faite depuis bash (ex. reconstruction `python3` après troncature confirmée par `Read`).
|
||||||
|
|||||||
@@ -2086,4 +2086,69 @@ console.log('[DB] Migrations 2FA OK');
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Migration : plateforme_id en CASCADE (au lieu de RESTRICT) sur
|
||||||
|
// depots_retraits et investissements ────────────────────────────────────
|
||||||
|
// RESTRICT empêchait la suppression complète d'un compte (DELETE FROM users) :
|
||||||
|
// la cascade users→plateformes (user_id CASCADE) et users→investisseurs→
|
||||||
|
// depots_retraits/investissements (CASCADE) sont deux branches indépendantes
|
||||||
|
// de l'arbre de suppression ; SQLite peut supprimer une plateforme avant les
|
||||||
|
// lignes qui la référencent encore sur l'autre branche, ce qui déclenche le
|
||||||
|
// RESTRICT (erreur SQLITE_CONSTRAINT_TRIGGER). La protection "impossible de
|
||||||
|
// supprimer une plateforme qui a encore des données" est déplacée dans la
|
||||||
|
// route DELETE /api/plateformes/:id (vérification explicite avec message clair).
|
||||||
|
{
|
||||||
|
const fixPlateformeCascade = (tableName, indexStatements) => {
|
||||||
|
const row = db.prepare(
|
||||||
|
`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`
|
||||||
|
).get(tableName);
|
||||||
|
if (!row || !/REFERENCES\s+plateformes\(id\)\s+ON DELETE RESTRICT/i.test(row.sql)) return;
|
||||||
|
|
||||||
|
const tempName = `__repair_${tableName}`;
|
||||||
|
const nameRe = new RegExp(
|
||||||
|
`CREATE TABLE\\s+(?:IF NOT EXISTS\\s+)?["'\`\\[]?${tableName}["'\`\\]]?`, 'i'
|
||||||
|
);
|
||||||
|
if (!nameRe.test(row.sql)) {
|
||||||
|
console.error(`[DB] migration plateforme_id CASCADE : nom de table non reconnu dans le DDL de "${tableName}", migration ignorée.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fixedDdl = row.sql
|
||||||
|
.replace(nameRe, `CREATE TABLE "${tempName}"`)
|
||||||
|
.replace(/REFERENCES\s+plateformes\(id\)\s+ON DELETE RESTRICT/i, 'REFERENCES plateformes(id) ON DELETE CASCADE');
|
||||||
|
|
||||||
|
const colDefs = db.prepare(`PRAGMA table_info("${tableName}")`).all();
|
||||||
|
const colNames = colDefs.map(c => `"${c.name}"`).join(', ');
|
||||||
|
|
||||||
|
const idxs = db.prepare(
|
||||||
|
`SELECT name FROM sqlite_master WHERE type='index' AND tbl_name=? AND sql IS NOT NULL`
|
||||||
|
).all(tableName);
|
||||||
|
|
||||||
|
db.exec('PRAGMA foreign_keys = OFF');
|
||||||
|
db.exec(`DROP TABLE IF EXISTS "${tempName}"`);
|
||||||
|
db.exec(fixedDdl);
|
||||||
|
db.exec(`INSERT INTO "${tempName}" (${colNames}) SELECT ${colNames} FROM "${tableName}"`);
|
||||||
|
for (const idx of idxs) db.exec(`DROP INDEX IF EXISTS "${idx.name}"`);
|
||||||
|
db.exec(`DROP TABLE "${tableName}"`);
|
||||||
|
db.exec('PRAGMA legacy_alter_table = ON');
|
||||||
|
db.exec(`ALTER TABLE "${tempName}" RENAME TO "${tableName}"`);
|
||||||
|
db.exec('PRAGMA legacy_alter_table = OFF');
|
||||||
|
db.exec('PRAGMA foreign_keys = ON');
|
||||||
|
|
||||||
|
for (const stmt of indexStatements) db.exec(stmt);
|
||||||
|
console.log(`[DB] migration : plateforme_id passé en ON DELETE CASCADE sur "${tableName}".`);
|
||||||
|
};
|
||||||
|
|
||||||
|
fixPlateformeCascade('depots_retraits', [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_depret_inv ON depots_retraits(investisseur_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_depret_plat ON depots_retraits(plateforme_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_depret_date ON depots_retraits(date_operation)',
|
||||||
|
]);
|
||||||
|
|
||||||
|
fixPlateformeCascade('investissements', [
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_inv_inv ON investissements(investisseur_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_inv_plat ON investissements(plateforme_id)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_inv_statut ON investissements(statut)',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_inv_date ON investissements(date_souscription)',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ CREATE INDEX IF NOT EXISTS idx_plateformes_user ON plateformes(user_id);
|
|||||||
CREATE TABLE IF NOT EXISTS depots_retraits (
|
CREATE TABLE IF NOT EXISTS depots_retraits (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
investisseur_id INTEGER NOT NULL REFERENCES investisseurs(id) ON DELETE CASCADE,
|
investisseur_id INTEGER NOT NULL REFERENCES investisseurs(id) ON DELETE CASCADE,
|
||||||
plateforme_id INTEGER NOT NULL REFERENCES plateformes(id) ON DELETE RESTRICT,
|
plateforme_id INTEGER NOT NULL REFERENCES plateformes(id) ON DELETE CASCADE,
|
||||||
date_operation TEXT NOT NULL, -- ISO YYYY-MM-DD
|
date_operation TEXT NOT NULL, -- ISO YYYY-MM-DD
|
||||||
type TEXT NOT NULL CHECK(type IN ('depot','retrait')),
|
type TEXT NOT NULL CHECK(type IN ('depot','retrait')),
|
||||||
montant REAL NOT NULL CHECK(montant >= 0),
|
montant REAL NOT NULL CHECK(montant >= 0),
|
||||||
@@ -92,7 +92,7 @@ CREATE INDEX IF NOT EXISTS idx_depret_date ON depots_retraits(date_operation);
|
|||||||
CREATE TABLE IF NOT EXISTS investissements (
|
CREATE TABLE IF NOT EXISTS investissements (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
investisseur_id INTEGER NOT NULL REFERENCES investisseurs(id) ON DELETE CASCADE,
|
investisseur_id INTEGER NOT NULL REFERENCES investisseurs(id) ON DELETE CASCADE,
|
||||||
plateforme_id INTEGER NOT NULL REFERENCES plateformes(id) ON DELETE RESTRICT,
|
plateforme_id INTEGER NOT NULL REFERENCES plateformes(id) ON DELETE CASCADE,
|
||||||
nom_projet TEXT NOT NULL,
|
nom_projet TEXT NOT NULL,
|
||||||
emetteur TEXT, -- nom de la société emprunteuse
|
emetteur TEXT, -- nom de la société emprunteuse
|
||||||
date_souscription TEXT NOT NULL,
|
date_souscription TEXT NOT NULL,
|
||||||
|
|||||||
@@ -679,6 +679,16 @@ router.delete('/:id', (req, res, next) => {
|
|||||||
const plat = db.prepare('SELECT logo_filename FROM plateformes WHERE id=? AND user_id=?')
|
const plat = db.prepare('SELECT logo_filename FROM plateformes WHERE id=? AND user_id=?')
|
||||||
.get(req.params.id, req.user.id);
|
.get(req.params.id, req.user.id);
|
||||||
if (!plat) throw new HttpError(404, 'Not found');
|
if (!plat) throw new HttpError(404, 'Not found');
|
||||||
|
|
||||||
|
// plateforme_id est en ON DELETE CASCADE sur investissements/depots_retraits (nécessaire pour
|
||||||
|
// permettre la suppression complète d'un compte) — on protège donc ici, explicitement, contre
|
||||||
|
// la suppression accidentelle d'une plateforme qui a encore des données rattachées.
|
||||||
|
const { n: nbInv } = db.prepare('SELECT COUNT(*) AS n FROM investissements WHERE plateforme_id = ?').get(req.params.id);
|
||||||
|
const { n: nbDr } = db.prepare('SELECT COUNT(*) AS n FROM depots_retraits WHERE plateforme_id = ?').get(req.params.id);
|
||||||
|
if (nbInv > 0 || nbDr > 0) {
|
||||||
|
throw new HttpError(400, `Impossible de supprimer cette plateforme : elle a encore ${nbInv} investissement(s) et ${nbDr} mouvement(s) de dépôt/retrait enregistrés. Supprimez-les d'abord.`);
|
||||||
|
}
|
||||||
|
|
||||||
const r = db.prepare('DELETE FROM plateformes WHERE id=? AND user_id=?')
|
const r = db.prepare('DELETE FROM plateformes WHERE id=? AND user_id=?')
|
||||||
.run(req.params.id, req.user.id);
|
.run(req.params.id, req.user.id);
|
||||||
if (r.changes === 0) throw new HttpError(404, 'Not found');
|
if (r.changes === 0) throw new HttpError(404, 'Not found');
|
||||||
|
|||||||
Reference in New Issue
Block a user