Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b481c4cc1d | |||
| 563063ba17 | |||
| 3c5aa635c4 |
@@ -1,5 +1,5 @@
|
|||||||
# MEMORY.md — Crowdlending Tracker
|
# 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
|
- `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é)
|
- Register/ResetPassword/InvitationRegister : via `appInfo.minPasswordLength || 8` (appInfo déjà fetché)
|
||||||
- MonCompte (SecurityForm) et UsersSection (CreateUserModal) : state local `minPasswordLength` + useEffect fetch dédié
|
- 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 `<input type="password">` 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'`<input>` interne ; `wrapperStyle` optionnel pour le `<div style="position:relative">` 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 `<PasswordInput>` plutôt que `<input type="password">` 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`.
|
||||||
|
|||||||
@@ -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;
|
export default db;
|
||||||
|
|||||||
@@ -107,10 +107,17 @@ router.post('/users', (req, res, next) => {
|
|||||||
const userId = result.lastInsertRowid;
|
const userId = result.lastInsertRowid;
|
||||||
const fullName = body.displayName || body.email.split('@')[0];
|
const fullName = body.displayName || body.email.split('@')[0];
|
||||||
const prenom = fullName.includes(' ') ? fullName.split(' ')[0] : null;
|
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);
|
).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 } });
|
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 });
|
res.status(201).json({ id: userId, email: body.email, display_name: body.displayName || null, role: body.role });
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
|
|||||||
@@ -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);
|
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é.');
|
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
|
// Finaliser le compte : mot de passe, nom, vérification
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
@@ -177,7 +178,18 @@ router.post('/:token/register', async (req, res, next) => {
|
|||||||
email_verified = 1,
|
email_verified = 1,
|
||||||
updated_at = datetime('now')
|
updated_at = datetime('now')
|
||||||
WHERE id = ?
|
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
|
// Marquer l'invitation comme utilisée
|
||||||
db.prepare("UPDATE invitations SET used_at = datetime('now') WHERE id = ?").run(inv.id);
|
db.prepare("UPDATE invitations SET used_at = datetime('now') WHERE id = ?").run(inv.id);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default function EmptyState({
|
|||||||
title = 'Aucune plateforme configurée',
|
title = 'Aucune plateforme configurée',
|
||||||
message = "Vous n'avez pas encore configuré de plateforme de crowdfunding sur votre compte. Associez au moins une plateforme pour commencer à suivre vos investissements.",
|
message = "Vous n'avez pas encore configuré de plateforme de crowdfunding sur votre compte. Associez au moins une plateforme pour commencer à suivre vos investissements.",
|
||||||
cta = 'Configurer mes plateformes',
|
cta = 'Configurer mes plateformes',
|
||||||
to = '/settings?section=plateformes',
|
to = '/settings?section=plateformes&openAdd=1',
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Champ mot de passe avec bouton "afficher/masquer".
|
||||||
|
* Utilisation : remplacer <input type="password" .../> par <PasswordInput .../>
|
||||||
|
* Toutes les props (className, value, onChange, required, autoComplete, style, etc.)
|
||||||
|
* sont transmises telles quelles à l'<input> sous-jacent.
|
||||||
|
*/
|
||||||
|
function EyeIcon({ open }) {
|
||||||
|
return open ? (
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z" stroke="var(--text-muted)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<circle cx="12" cy="12" r="3" stroke="var(--text-muted)" strokeWidth="2"/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M17.94 17.94A10.94 10.94 0 0112 19c-7 0-11-7-11-7a20.6 20.6 0 015.06-5.94M9.9 4.24A10.9 10.9 0 0112 4c7 0 11 7 11 7a20.6 20.6 0 01-2.66 3.78M14.12 14.12a3 3 0 11-4.24-4.24" stroke="var(--text-muted)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<line x1="1" y1="1" x2="23" y2="23" stroke="var(--text-muted)" strokeWidth="2" strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PasswordInput({ style, wrapperStyle, ...props }) {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative', ...wrapperStyle }}>
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
type={show ? 'text' : 'password'}
|
||||||
|
style={{ ...style, paddingRight: 40 }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={() => setShow(v => !v)}
|
||||||
|
aria-label={show ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
|
||||||
|
style={{
|
||||||
|
position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)',
|
||||||
|
background: 'none', border: 'none', padding: 4, display: 'flex',
|
||||||
|
alignItems: 'center', justifyContent: 'center', cursor: 'pointer', lineHeight: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EyeIcon open={show} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import PasswordStrength from '../components/PasswordStrength.jsx';
|
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||||
|
import PasswordInput from '../components/PasswordInput.jsx';
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||||
|
|
||||||
// Wrap défini en dehors du composant pour éviter le remontage à chaque frappe
|
// Wrap défini en dehors du composant pour éviter le remontage à chaque frappe
|
||||||
@@ -173,13 +174,13 @@ export default function InvitationRegister() {
|
|||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Mot de passe <span style={{ color: 'var(--text-muted)', fontWeight: 400 }}>(8 car. min.)</span></label>
|
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Mot de passe <span style={{ color: 'var(--text-muted)', fontWeight: 400 }}>(8 car. min.)</span></label>
|
||||||
<input className="form-input" type="password" required minLength={appInfo.minPasswordLength || 8} autoComplete="new-password" placeholder="••••••••" value={form.password} onChange={e => set('password', e.target.value)} style={{ width: '100%' }} />
|
<PasswordInput className="form-input" required minLength={appInfo.minPasswordLength || 8} autoComplete="new-password" placeholder="••••••••" value={form.password} onChange={e => set('password', e.target.value)} style={{ width: '100%' }} />
|
||||||
<PasswordStrength password={form.password} minLength={appInfo.minPasswordLength || 8} />
|
<PasswordStrength password={form.password} minLength={appInfo.minPasswordLength || 8} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Confirmer le mot de passe</label>
|
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Confirmer le mot de passe</label>
|
||||||
<input className="form-input" type="password" required minLength={appInfo.minPasswordLength || 8} autoComplete="new-password" placeholder="••••••••" value={form.confirm} onChange={e => set('confirm', e.target.value)} style={{ width: '100%' }} />
|
<PasswordInput className="form-input" required minLength={appInfo.minPasswordLength || 8} autoComplete="new-password" placeholder="••••••••" value={form.confirm} onChange={e => set('confirm', e.target.value)} style={{ width: '100%' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" disabled={busy} style={{ marginTop: 4, width: '100%', padding: '11px 0', background: busy ? 'var(--text-muted)' : 'var(--primary, #1e40af)', color: '#fff', border: 'none', borderRadius: 8, fontSize: 15, fontWeight: 600, cursor: busy ? 'not-allowed' : 'pointer', transition: 'background 0.15s' }}>
|
<button type="submit" disabled={busy} style={{ marginTop: 4, width: '100%', padding: '11px 0', background: busy ? 'var(--text-muted)' : 'var(--primary, #1e40af)', color: '#fff', border: 'none', borderRadius: 8, fontSize: 15, fontWeight: 600, cursor: busy ? 'not-allowed' : 'pointer', transition: 'background 0.15s' }}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
|||||||
import { useAuth } from '../context/AuthContext.jsx';
|
import { useAuth } from '../context/AuthContext.jsx';
|
||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
import AuthBgCol from '../components/AuthBgCol.jsx';
|
import AuthBgCol from '../components/AuthBgCol.jsx';
|
||||||
|
import PasswordInput from '../components/PasswordInput.jsx';
|
||||||
|
|
||||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
const DEVICE_KEY = 'cl_device_token';
|
const DEVICE_KEY = 'cl_device_token';
|
||||||
@@ -217,7 +218,7 @@ export default function Login() {
|
|||||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Mot de passe</label>
|
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Mot de passe</label>
|
||||||
<Link to="/forgot-password" style={{ fontSize: 13, color: 'var(--text-muted)', textDecoration: 'underline' }}>Mot de passe oublié ?</Link>
|
<Link to="/forgot-password" style={{ fontSize: 13, color: 'var(--text-muted)', textDecoration: 'underline' }}>Mot de passe oublié ?</Link>
|
||||||
</div>
|
</div>
|
||||||
<input className="form-input" type="password" required autoComplete="current-password" placeholder="••••••••"
|
<PasswordInput className="form-input" required autoComplete="current-password" placeholder="••••••••"
|
||||||
value={password} onChange={e => setPassword(e.target.value)} style={{ width: '100%' }} />
|
value={password} onChange={e => setPassword(e.target.value)} style={{ width: '100%' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import PasswordStrength from '../components/PasswordStrength.jsx';
|
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||||
|
import PasswordInput from '../components/PasswordInput.jsx';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext.jsx';
|
import { useAuth } from '../context/AuthContext.jsx';
|
||||||
import { useUi } from '../context/UiContext.jsx';
|
import { useUi } from '../context/UiContext.jsx';
|
||||||
@@ -143,7 +144,7 @@ function EmailChangeForm({ onDone }) {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
<label className="profile-label">Mot de passe actuel (confirmation)</label>
|
<label className="profile-label">Mot de passe actuel (confirmation)</label>
|
||||||
<input className="profile-input" type="password" required value={pwd} onChange={e => setPwd(e.target.value)} placeholder="••••••••" />
|
<PasswordInput className="profile-input" required value={pwd} onChange={e => setPwd(e.target.value)} placeholder="••••••••" />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<button type="submit" className="btn btn-primary" disabled={busy} style={{ fontSize: 13 }}>
|
<button type="submit" className="btn btn-primary" disabled={busy} style={{ fontSize: 13 }}>
|
||||||
@@ -389,7 +390,7 @@ function TwoFASection({ user }) {
|
|||||||
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
|
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<label style={{ display: 'block', marginBottom: 4, fontSize: 13, fontWeight: 500 }}>Mot de passe actuel</label>
|
<label style={{ display: 'block', marginBottom: 4, fontSize: 13, fontWeight: 500 }}>Mot de passe actuel</label>
|
||||||
<input type="password" required value={disablePwd}
|
<PasswordInput required value={disablePwd}
|
||||||
onChange={e => setDisablePwd(e.target.value)}
|
onChange={e => setDisablePwd(e.target.value)}
|
||||||
autoComplete="current-password" placeholder="••••••••" />
|
autoComplete="current-password" placeholder="••••••••" />
|
||||||
</div>
|
</div>
|
||||||
@@ -469,20 +470,20 @@ function SecurityForm() {
|
|||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 400 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 400 }}>
|
||||||
<div>
|
<div>
|
||||||
<label>Mot de passe actuel</label>
|
<label>Mot de passe actuel</label>
|
||||||
<input type="password" required autoComplete="current-password"
|
<PasswordInput required autoComplete="current-password"
|
||||||
value={pwdForm.currentPassword}
|
value={pwdForm.currentPassword}
|
||||||
onChange={e => setPwdForm({ ...pwdForm, currentPassword: e.target.value })} />
|
onChange={e => setPwdForm({ ...pwdForm, currentPassword: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Nouveau mot de passe</label>
|
<label>Nouveau mot de passe</label>
|
||||||
<input type="password" required autoComplete="new-password"
|
<PasswordInput required autoComplete="new-password"
|
||||||
value={pwdForm.newPassword}
|
value={pwdForm.newPassword}
|
||||||
onChange={e => setPwdForm({ ...pwdForm, newPassword: e.target.value })} />
|
onChange={e => setPwdForm({ ...pwdForm, newPassword: e.target.value })} />
|
||||||
<PasswordStrength password={pwdForm.newPassword} minLength={minPasswordLength} />
|
<PasswordStrength password={pwdForm.newPassword} minLength={minPasswordLength} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Confirmer le nouveau mot de passe</label>
|
<label>Confirmer le nouveau mot de passe</label>
|
||||||
<input type="password" required autoComplete="new-password"
|
<PasswordInput required autoComplete="new-password"
|
||||||
value={pwdForm.confirm}
|
value={pwdForm.confirm}
|
||||||
onChange={e => setPwdForm({ ...pwdForm, confirm: e.target.value })} />
|
onChange={e => setPwdForm({ ...pwdForm, confirm: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
|||||||
import { useAuth } from '../context/AuthContext.jsx';
|
import { useAuth } from '../context/AuthContext.jsx';
|
||||||
import PasswordStrength from '../components/PasswordStrength.jsx';
|
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||||
import AuthBgCol from '../components/AuthBgCol.jsx';
|
import AuthBgCol from '../components/AuthBgCol.jsx';
|
||||||
|
import PasswordInput from '../components/PasswordInput.jsx';
|
||||||
|
|
||||||
export default function Register() {
|
export default function Register() {
|
||||||
const { register } = useAuth();
|
const { register } = useAuth();
|
||||||
@@ -178,9 +179,9 @@ export default function Register() {
|
|||||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
|
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
|
||||||
Mot de passe
|
Mot de passe
|
||||||
</label>
|
</label>
|
||||||
<input
|
<PasswordInput
|
||||||
className="form-input"
|
className="form-input"
|
||||||
type="password" required
|
required
|
||||||
minLength={appInfo.minPasswordLength || 8}
|
minLength={appInfo.minPasswordLength || 8}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import PasswordStrength from '../components/PasswordStrength.jsx';
|
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||||
import AuthBgCol from '../components/AuthBgCol.jsx';
|
import AuthBgCol from '../components/AuthBgCol.jsx';
|
||||||
|
import PasswordInput from '../components/PasswordInput.jsx';
|
||||||
|
|
||||||
export default function ResetPassword() {
|
export default function ResetPassword() {
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
@@ -154,9 +155,9 @@ export default function ResetPassword() {
|
|||||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
|
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
|
||||||
Nouveau mot de passe
|
Nouveau mot de passe
|
||||||
</label>
|
</label>
|
||||||
<input
|
<PasswordInput
|
||||||
className="form-input"
|
className="form-input"
|
||||||
type="password" required minLength={appInfo.minPasswordLength || 8}
|
required minLength={appInfo.minPasswordLength || 8}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={password}
|
value={password}
|
||||||
@@ -170,9 +171,9 @@ export default function ResetPassword() {
|
|||||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
|
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
|
||||||
Confirmer le mot de passe
|
Confirmer le mot de passe
|
||||||
</label>
|
</label>
|
||||||
<input
|
<PasswordInput
|
||||||
className="form-input"
|
className="form-input"
|
||||||
type="password" required minLength={appInfo.minPasswordLength || 8}
|
required minLength={appInfo.minPasswordLength || 8}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={password2}
|
value={password2}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { api } from '../../api.js';
|
import { api } from '../../api.js';
|
||||||
|
import PasswordInput from '../../components/PasswordInput.jsx';
|
||||||
|
|
||||||
export default function CreateUserSection({ onCreated }) {
|
export default function CreateUserSection({ onCreated }) {
|
||||||
const [form, setForm] = useState({ email: '', password: '', displayName: '', role: 'user' });
|
const [form, setForm] = useState({ email: '', password: '', displayName: '', role: 'user' });
|
||||||
@@ -48,7 +49,7 @@ export default function CreateUserSection({ onCreated }) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Mot de passe *</label>
|
<label>Mot de passe *</label>
|
||||||
<input type="password" required minLength={8} value={form.password} onChange={e => set('password', e.target.value)} placeholder="8 caractères minimum" />
|
<PasswordInput required minLength={8} value={form.password} onChange={e => set('password', e.target.value)} placeholder="8 caractères minimum" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Rôle</label>
|
<label>Rôle</label>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import ConfirmModal from '../../components/ConfirmModal.jsx';
|
|||||||
import Modal from '../../components/Modal.jsx';
|
import Modal from '../../components/Modal.jsx';
|
||||||
import ResultBanner from '../../components/ResultBanner.jsx';
|
import ResultBanner from '../../components/ResultBanner.jsx';
|
||||||
import PasswordStrength from '../../components/PasswordStrength.jsx';
|
import PasswordStrength from '../../components/PasswordStrength.jsx';
|
||||||
|
import PasswordInput from '../../components/PasswordInput.jsx';
|
||||||
import { fmt, Badge, UserStatusBadge } from './adminHelpers.jsx';
|
import { fmt, Badge, UserStatusBadge } from './adminHelpers.jsx';
|
||||||
|
|
||||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
@@ -363,7 +364,7 @@ function CreateUserModal({ open, onClose, onCreated }) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Mot de passe *</label>
|
<label>Mot de passe *</label>
|
||||||
<input type="password" required minLength={minPasswordLength} autoComplete="new-password" value={form.password} onChange={e => set('password', e.target.value)} placeholder={`${minPasswordLength} caractères minimum`} />
|
<PasswordInput required minLength={minPasswordLength} autoComplete="new-password" value={form.password} onChange={e => set('password', e.target.value)} placeholder={`${minPasswordLength} caractères minimum`} />
|
||||||
<PasswordStrength password={form.password} minLength={minPasswordLength} />
|
<PasswordStrength password={form.password} minLength={minPasswordLength} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { api } from '../../api.js';
|
import { api } from '../../api.js';
|
||||||
import InvSelect from '../../components/InvSelect.jsx';
|
import InvSelect from '../../components/InvSelect.jsx';
|
||||||
import Modal from '../../components/Modal.jsx';
|
import Modal from '../../components/Modal.jsx';
|
||||||
@@ -629,6 +629,8 @@ function PlatForm({ state, setter, logoFile, setLogoFile, logoPreview, setLogoPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PlateformesSection() {
|
export default function PlateformesSection() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
// ── State ──────────────────────────────────────────────────────
|
// ── State ──────────────────────────────────────────────────────
|
||||||
const [plats, setPlats] = useState([]);
|
const [plats, setPlats] = useState([]);
|
||||||
const [investisseurs, setInvestisseurs] = useState([]);
|
const [investisseurs, setInvestisseurs] = useState([]);
|
||||||
@@ -693,6 +695,17 @@ export default function PlateformesSection() {
|
|||||||
setSelectedPlat(prev => prev ? prev : plats[0]);
|
setSelectedPlat(prev => prev ? prev : plats[0]);
|
||||||
}, [plats]); // eslint-disable-line
|
}, [plats]); // eslint-disable-line
|
||||||
|
|
||||||
|
// Ouverture automatique du picker d'ajout (venant de l'état vide "Aucune plateforme configurée")
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchParams.get('openAdd') === '1') {
|
||||||
|
setErr(null);
|
||||||
|
setShowAddPicker(true);
|
||||||
|
const next = new URLSearchParams(searchParams);
|
||||||
|
next.delete('openAdd');
|
||||||
|
setSearchParams(next, { replace: true });
|
||||||
|
}
|
||||||
|
}, [searchParams]); // eslint-disable-line
|
||||||
|
|
||||||
const handleLogoFile = (file, setFile, setPreview) => {
|
const handleLogoFile = (file, setFile, setPreview) => {
|
||||||
if (!file) { setFile(null); setPreview(null); return; }
|
if (!file) { setFile(null); setPreview(null); return; }
|
||||||
setFile(file);
|
setFile(file);
|
||||||
|
|||||||
Reference in New Issue
Block a user