diff --git a/MEMORY.md b/MEMORY.md
index 1a3738f..680fc99 100644
--- a/MEMORY.md
+++ b/MEMORY.md
@@ -1,5 +1,5 @@
# MEMORY.md — Crowdlending Tracker
-*Dernière mise à jour: 2026-06-15 (session 7)*
+*Dernière mise à jour: 2026-07-03 (session 8)*
---
@@ -425,3 +425,29 @@ const isBonus = BONUS_VALUES.includes(form.investissement_id);
- `useState(8)` comme valeur initiale sur tous les écrans → jamais bloquant si l'API est lente
- Register/ResetPassword/InvitationRegister : via `appInfo.minPasswordLength || 8` (appInfo déjà fetché)
- MonCompte (SecurityForm) et UsersSection (CreateUserModal) : state local `minPasswordLength` + useEffect fetch dédié
+
+---
+
+## Session 8 — UX empty state + visibilité mot de passe (2026-07-03)
+
+### EmptyState → ouverture directe de l'ajout de plateforme
+- `EmptyState.jsx` : prop `to` par défaut passée de `/settings?section=plateformes` à `/settings?section=plateformes&openAdd=1`
+- `PlateformesSection.jsx` : `useSearchParams` + `useEffect` détecte `openAdd=1` au montage → ouvre directement `showAddPicker` (modale "Ajouter une plateforme") → nettoie le paramètre de l'URL (`replace: true`) pour éviter la réouverture au refresh
+- Évite l'étape intermédiaire où l'utilisateur devait cliquer une seconde fois sur "+ Ajouter" après avoir été redirigé depuis un état vide (Dashboard, Investissements, Remboursements, DepotsRetraits)
+- **Pattern réutilisable** : pour toute redirection "action directe" similaire depuis un état vide, ajouter un query param dédié + `useEffect` de consommation/nettoyage dans la section cible
+
+### Composant PasswordInput — afficher/masquer mot de passe
+- Nouveau composant `frontend/src/components/PasswordInput.jsx` : wrapper autour d'un `` avec bouton œil (SVG inline, pas de lib externe) togglant `type` entre `password`/`text`
+- API : toutes les props (`className`, `value`, `onChange`, `required`, `autoComplete`, `minLength`, `style`, etc.) sont transmises telles quelles à l'`` interne ; `wrapperStyle` optionnel pour le `
` englobant
+- **Déployé sur les 8 écrans contenant un champ mot de passe** : Login, Register, ResetPassword (×2 champs), InvitationRegister (×2 champs), MonCompte (SecurityForm ×3 champs + changement email ×1 + désactivation 2FA ×1), admin/CreateUserSection, admin/UsersSection
+- **Règle à respecter** : tout nouveau champ mot de passe doit utiliser `` plutôt que `` brut, pour garder l'UX cohérente sur toute l'app
+
+### Bug — profil principal / compte courant non créés hors /auth/register
+- **Constat** : seul `/api/auth/register` (auto-inscription) créait le profil investisseur principal (`is_principal=1`) ET le compte courant associé. Les deux autres parcours de création de compte en étaient dépourvus :
+ - `POST /api/admin/users` (admin.js, `CreateUserSection.jsx`) : créait l'investisseur mais **sans `is_principal=1`** et **sans compte courant**
+ - `POST /api/invitations/:token/register` (invitations.js, `InvitationRegister.jsx`) : ne créait **aucun investisseur ni compte courant**
+- **Cas réel trouvé en base** : `marine@croguennec.net` (user #2, créée par invitation le 2026-06-18) n'avait **aucun** investisseur ; `newargus@gmail.com` (user #3, créé par l'admin le 2026-07-03) avait un investisseur mais `is_principal=0` et zéro compte
+- **Fix appliqué** :
+ 1. `admin.js` (`POST /users`) et `invitations.js` (`POST /:token/register`) répliquent maintenant exactement la logique de `auth.js` : `INSERT INTO investisseurs (..., is_principal) VALUES (..., 1)` + `INSERT INTO comptes (user_id, nom, type, investisseur_id)` avec `nom = 'Compte courant — ' + fullName`
+ 2. Backfill idempotent ajouté en fin de `backend/src/db/index.js` (avant `export default db`) qui tourne à chaque démarrage : (1) crée un investisseur principal pour tout user qui n'en a aucun, (2) marque principal le plus ancien investisseur `famille` pour tout user qui n'a pas de principal, (3) crée le compte courant manquant pour tout investisseur principal qui n'en a pas. Toutes les requêtes utilisent `NOT EXISTS` → sans effet une fois les données corrigées.
+- **Règle à retenir** : toute nouvelle voie de création de compte utilisateur doit répliquer les 2 inserts de `auth.js` (`investisseurs` avec `is_principal=1` + `comptes` type `compte_courant`) — ne pas dupliquer seulement l'insert `investisseurs`.
diff --git a/backend/src/db/index.js b/backend/src/db/index.js
index deafeaf..8da3af9 100644
--- a/backend/src/db/index.js
+++ b/backend/src/db/index.js
@@ -2012,4 +2012,54 @@ console.log('[DB] Migrations 2FA OK');
}
}
+// ── Backfill : garantir un profil principal + un compte courant par utilisateur ──
+// Certains parcours de création de compte (admin, invitation) ne créaient pas
+// systématiquement le profil investisseur principal et/ou son compte courant,
+// contrairement à /auth/register. Ce backfill est idempotent (NOT EXISTS partout)
+// et s'exécute à chaque démarrage pour rattraper les comptes existants.
+{
+ // 1) Utilisateurs sans aucun investisseur (ex : comptes créés via invitation)
+ const usersWithoutInvestisseur = db.prepare(`
+ SELECT id, email, display_name FROM users u
+ WHERE NOT EXISTS (SELECT 1 FROM investisseurs i WHERE i.user_id = u.id)
+ `).all();
+ for (const u of usersWithoutInvestisseur) {
+ const fullName = u.display_name || u.email.split('@')[0];
+ const prenom = fullName.includes(' ') ? fullName.split(' ')[0] : null;
+ db.prepare(
+ `INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, is_principal) VALUES (?, ?, ?, 'famille', 'PP', 1)`
+ ).run(u.id, fullName, prenom);
+ console.log(`[DB] backfill: profil investisseur principal créé pour user #${u.id} (${u.email})`);
+ }
+
+ // 2) Utilisateurs ayant des investisseurs mais aucun marqué principal (ex : comptes créés par un admin)
+ const usersWithoutPrincipal = db.prepare(`
+ SELECT DISTINCT user_id FROM investisseurs i
+ WHERE NOT EXISTS (SELECT 1 FROM investisseurs p WHERE p.user_id = i.user_id AND p.is_principal = 1)
+ `).all();
+ for (const { user_id } of usersWithoutPrincipal) {
+ const candidate = db.prepare(`
+ SELECT id FROM investisseurs WHERE user_id = ? ORDER BY (type = 'famille') DESC, id ASC LIMIT 1
+ `).get(user_id);
+ if (candidate) {
+ db.prepare('UPDATE investisseurs SET is_principal = 1 WHERE id = ?').run(candidate.id);
+ console.log(`[DB] backfill: investisseur #${candidate.id} marqué principal pour user #${user_id}`);
+ }
+ }
+
+ // 3) Investisseurs principaux sans compte courant
+ const principalsWithoutCompte = db.prepare(`
+ SELECT i.id AS investisseur_id, i.user_id, i.nom
+ FROM investisseurs i
+ WHERE i.is_principal = 1
+ AND NOT EXISTS (SELECT 1 FROM comptes c WHERE c.investisseur_id = i.id)
+ `).all();
+ for (const inv of principalsWithoutCompte) {
+ db.prepare(
+ 'INSERT INTO comptes (user_id, nom, type, investisseur_id) VALUES (?,?,?,?)'
+ ).run(inv.user_id, `Compte courant — ${inv.nom}`, 'compte_courant', inv.investisseur_id);
+ console.log(`[DB] backfill: compte courant créé pour l'investisseur principal #${inv.investisseur_id} (user #${inv.user_id})`);
+ }
+}
+
export default db;
diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js
index 547472d..822be6f 100644
--- a/backend/src/routes/admin.js
+++ b/backend/src/routes/admin.js
@@ -107,10 +107,17 @@ router.post('/users', (req, res, next) => {
const userId = result.lastInsertRowid;
const fullName = body.displayName || body.email.split('@')[0];
const prenom = fullName.includes(' ') ? fullName.split(' ')[0] : null;
- db.prepare(
- `INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal) VALUES (?, ?, ?, 'famille', 'PP')`
+
+ // Auto-créer le profil principal (= l'utilisateur lui-même), comme dans /auth/register
+ const invResult = db.prepare(
+ `INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, is_principal) VALUES (?, ?, ?, 'famille', 'PP', 1)`
).run(userId, fullName, prenom);
+ // Auto-créer un compte courant pour le profil principal, comme dans /auth/register
+ db.prepare(
+ 'INSERT INTO comptes (user_id, nom, type, investisseur_id) VALUES (?,?,?,?)'
+ ).run(userId, `Compte courant — ${fullName}`, 'compte_courant', invResult.lastInsertRowid);
+
audit(req, { action: 'user_created', category: 'account', actorId: req.user.id, targetUserId: userId, details: { email: body.email, role: body.role, created_by_admin: true } });
res.status(201).json({ id: userId, email: body.email, display_name: body.displayName || null, role: body.role });
} catch (e) { next(e); }
diff --git a/backend/src/routes/invitations.js b/backend/src/routes/invitations.js
index 61a8a2b..c86badf 100644
--- a/backend/src/routes/invitations.js
+++ b/backend/src/routes/invitations.js
@@ -167,7 +167,8 @@ router.post('/:token/register', async (req, res, next) => {
const pendingUser = db.prepare('SELECT id FROM users WHERE LOWER(email) = LOWER(?) AND email_verified = 0').get(inv.email);
if (!pendingUser) throw new HttpError(409, 'Ce compte a déjà été activé ou supprimé.');
- const hash = await bcrypt.hash(password, 12);
+ const hash = await bcrypt.hash(password, 12);
+ const fullName = displayName || nameFromEmail(inv.email);
// Finaliser le compte : mot de passe, nom, vérification
db.prepare(`
@@ -177,7 +178,18 @@ router.post('/:token/register', async (req, res, next) => {
email_verified = 1,
updated_at = datetime('now')
WHERE id = ?
- `).run(hash, displayName || nameFromEmail(inv.email), pendingUser.id);
+ `).run(hash, fullName, pendingUser.id);
+
+ // Auto-créer le profil principal (= l'utilisateur lui-même) + son compte courant,
+ // comme dans /auth/register — sans quoi l'utilisateur invité se retrouve sans
+ // aucun investisseur ni compte courant à sa première connexion.
+ const prenom = fullName.includes(' ') ? fullName.split(' ')[0] : null;
+ const invResult = db.prepare(
+ `INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, is_principal) VALUES (?, ?, ?, 'famille', 'PP', 1)`
+ ).run(pendingUser.id, fullName, prenom);
+ db.prepare(
+ 'INSERT INTO comptes (user_id, nom, type, investisseur_id) VALUES (?,?,?,?)'
+ ).run(pendingUser.id, `Compte courant — ${fullName}`, 'compte_courant', invResult.lastInsertRowid);
// Marquer l'invitation comme utilisée
db.prepare("UPDATE invitations SET used_at = datetime('now') WHERE id = ?").run(inv.id);
diff --git a/frontend/src/pages/admin/CreateUserSection.jsx b/frontend/src/pages/admin/CreateUserSection.jsx
index 30eefd3..ddae4e0 100644
--- a/frontend/src/pages/admin/CreateUserSection.jsx
+++ b/frontend/src/pages/admin/CreateUserSection.jsx
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { api } from '../../api.js';
+import PasswordInput from '../../components/PasswordInput.jsx';
export default function CreateUserSection({ onCreated }) {
const [form, setForm] = useState({ email: '', password: '', displayName: '', role: 'user' });
@@ -48,7 +49,7 @@ export default function CreateUserSection({ onCreated }) {