Compare commits
3 Commits
31d7b8f791
...
b481c4cc1d
| Author | SHA1 | Date | |
|---|---|---|---|
| b481c4cc1d | |||
| 563063ba17 | |||
| 3c5aa635c4 |
@@ -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 `<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;
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -168,6 +168,7 @@ router.post('/:token/register', async (req, res, next) => {
|
||||
if (!pendingUser) throw new HttpError(409, 'Ce compte a déjà été activé ou supprimé.');
|
||||
|
||||
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);
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function EmptyState({
|
||||
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.",
|
||||
cta = 'Configurer mes plateformes',
|
||||
to = '/settings?section=plateformes',
|
||||
to = '/settings?section=plateformes&openAdd=1',
|
||||
}) {
|
||||
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 PasswordStrength from '../components/PasswordStrength.jsx';
|
||||
import PasswordInput from '../components/PasswordInput.jsx';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
|
||||
// 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 }}>
|
||||
<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} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<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>
|
||||
|
||||
<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 { api } from '../api.js';
|
||||
import AuthBgCol from '../components/AuthBgCol.jsx';
|
||||
import PasswordInput from '../components/PasswordInput.jsx';
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
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>
|
||||
<Link to="/forgot-password" style={{ fontSize: 13, color: 'var(--text-muted)', textDecoration: 'underline' }}>Mot de passe oublié ?</Link>
|
||||
</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%' }} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||
import PasswordInput from '../components/PasswordInput.jsx';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext.jsx';
|
||||
import { useUi } from '../context/UiContext.jsx';
|
||||
@@ -143,7 +144,7 @@ function EmailChangeForm({ onDone }) {
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<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 style={{ display: 'flex', gap: 8 }}>
|
||||
<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={{ flex: 1 }}>
|
||||
<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)}
|
||||
autoComplete="current-password" placeholder="••••••••" />
|
||||
</div>
|
||||
@@ -469,20 +470,20 @@ function SecurityForm() {
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 400 }}>
|
||||
<div>
|
||||
<label>Mot de passe actuel</label>
|
||||
<input type="password" required autoComplete="current-password"
|
||||
<PasswordInput required autoComplete="current-password"
|
||||
value={pwdForm.currentPassword}
|
||||
onChange={e => setPwdForm({ ...pwdForm, currentPassword: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Nouveau mot de passe</label>
|
||||
<input type="password" required autoComplete="new-password"
|
||||
<PasswordInput required autoComplete="new-password"
|
||||
value={pwdForm.newPassword}
|
||||
onChange={e => setPwdForm({ ...pwdForm, newPassword: e.target.value })} />
|
||||
<PasswordStrength password={pwdForm.newPassword} minLength={minPasswordLength} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Confirmer le nouveau mot de passe</label>
|
||||
<input type="password" required autoComplete="new-password"
|
||||
<PasswordInput required autoComplete="new-password"
|
||||
value={pwdForm.confirm}
|
||||
onChange={e => setPwdForm({ ...pwdForm, confirm: e.target.value })} />
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext.jsx';
|
||||
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||
import AuthBgCol from '../components/AuthBgCol.jsx';
|
||||
import PasswordInput from '../components/PasswordInput.jsx';
|
||||
|
||||
export default function Register() {
|
||||
const { register } = useAuth();
|
||||
@@ -178,9 +179,9 @@ export default function Register() {
|
||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
<PasswordInput
|
||||
className="form-input"
|
||||
type="password" required
|
||||
required
|
||||
minLength={appInfo.minPasswordLength || 8}
|
||||
autoComplete="new-password"
|
||||
placeholder="••••••••"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||
import AuthBgCol from '../components/AuthBgCol.jsx';
|
||||
import PasswordInput from '../components/PasswordInput.jsx';
|
||||
|
||||
export default function ResetPassword() {
|
||||
const [params] = useSearchParams();
|
||||
@@ -154,9 +155,9 @@ export default function ResetPassword() {
|
||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
|
||||
Nouveau mot de passe
|
||||
</label>
|
||||
<input
|
||||
<PasswordInput
|
||||
className="form-input"
|
||||
type="password" required minLength={appInfo.minPasswordLength || 8}
|
||||
required minLength={appInfo.minPasswordLength || 8}
|
||||
autoComplete="new-password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
@@ -170,9 +171,9 @@ export default function ResetPassword() {
|
||||
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
|
||||
Confirmer le mot de passe
|
||||
</label>
|
||||
<input
|
||||
<PasswordInput
|
||||
className="form-input"
|
||||
type="password" required minLength={appInfo.minPasswordLength || 8}
|
||||
required minLength={appInfo.minPasswordLength || 8}
|
||||
autoComplete="new-password"
|
||||
placeholder="••••••••"
|
||||
value={password2}
|
||||
|
||||
@@ -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 }) {
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<label>Rôle</label>
|
||||
|
||||
@@ -5,6 +5,7 @@ import ConfirmModal from '../../components/ConfirmModal.jsx';
|
||||
import Modal from '../../components/Modal.jsx';
|
||||
import ResultBanner from '../../components/ResultBanner.jsx';
|
||||
import PasswordStrength from '../../components/PasswordStrength.jsx';
|
||||
import PasswordInput from '../../components/PasswordInput.jsx';
|
||||
import { fmt, Badge, UserStatusBadge } from './adminHelpers.jsx';
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
@@ -363,7 +364,7 @@ function CreateUserModal({ open, onClose, onCreated }) {
|
||||
</div>
|
||||
<div>
|
||||
<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} />
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 InvSelect from '../../components/InvSelect.jsx';
|
||||
import Modal from '../../components/Modal.jsx';
|
||||
@@ -629,6 +629,8 @@ function PlatForm({ state, setter, logoFile, setLogoFile, logoPreview, setLogoPr
|
||||
}
|
||||
|
||||
export default function PlateformesSection() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
// ── State ──────────────────────────────────────────────────────
|
||||
const [plats, setPlats] = useState([]);
|
||||
const [investisseurs, setInvestisseurs] = useState([]);
|
||||
@@ -693,6 +695,17 @@ export default function PlateformesSection() {
|
||||
setSelectedPlat(prev => prev ? prev : plats[0]);
|
||||
}, [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) => {
|
||||
if (!file) { setFile(null); setPreview(null); return; }
|
||||
setFile(file);
|
||||
|
||||
Reference in New Issue
Block a user