Possibilité d'avoir une clé API Globale par famille
This commit is contained in:
@@ -2279,4 +2279,22 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_api_keys_inv ON api_keys(investisseur_id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)');
|
||||
|
||||
// ── Migration : clés API à scope "Famille et entreprises" ───────────────
|
||||
// Une clé peut désormais couvrir tous les investisseurs du foyer plutôt
|
||||
// qu'un seul (agrégation, en miroir du scope=all déjà utilisé par le
|
||||
// frontend JWT). `investisseur_id` reste NOT NULL pour ne pas toucher à la
|
||||
// contrainte existante : quand scope_all=1, la route de création force
|
||||
// investisseur_id à pointer vers l'investisseur principal (ancrage FK),
|
||||
// mais les routes /api/v1/* ignorent alors cette valeur au profit d'un
|
||||
// filtre "tous les investisseurs de ce user_id" — voir apiKey.js et
|
||||
// routes/v1/*.js. Seul le profil principal peut créer une clé scope_all=1
|
||||
// (enforcement côté serveur dans routes/apiKeys.js, pas seulement l'UI).
|
||||
{
|
||||
const cols = db.prepare('PRAGMA table_info(api_keys)').all().map(c => c.name);
|
||||
if (!cols.includes('scope_all')) {
|
||||
db.exec('ALTER TABLE api_keys ADD COLUMN scope_all INTEGER NOT NULL DEFAULT 0');
|
||||
console.log('[DB] api_keys: colonne scope_all ajoutée');
|
||||
}
|
||||
}
|
||||
|
||||
export default db;
|
||||
|
||||
@@ -3,9 +3,16 @@ import db from '../db/index.js';
|
||||
|
||||
/**
|
||||
* Authentification par clé API (X-API-Key), distincte du JWT utilisé par le
|
||||
* frontend (requireAuth). Une clé API est toujours scopée à un seul
|
||||
* investisseur — pas de notion de "scope=all" ici, contrairement aux routes
|
||||
* internes. Réservé aux routes /api/v1 (lecture seule, Phase 1).
|
||||
* frontend (requireAuth). Une clé API est scopée soit à un seul investisseur,
|
||||
* soit à "Famille et entreprises" (scope_all=1, réservé au profil principal
|
||||
* — voir routes/apiKeys.js) — en miroir du scope=all des routes internes.
|
||||
* Réservé aux routes /api/v1 (lecture seule, Phase 1).
|
||||
*
|
||||
* Expose sur `req` :
|
||||
* - req.investisseurId : id de l'investisseur si scope unique, sinon null
|
||||
* - req.investisseurScopeAll: true si la clé couvre tout le foyer
|
||||
* - req.userId : user_id du titulaire de la clé (toujours défini,
|
||||
* utile pour le filtre "tous les investisseurs" quand scope_all)
|
||||
*/
|
||||
export function requireApiKey(req, res, next) {
|
||||
const key = req.header('X-API-Key');
|
||||
@@ -15,7 +22,7 @@ export function requireApiKey(req, res, next) {
|
||||
|
||||
const hash = crypto.createHash('sha256').update(key).digest('hex');
|
||||
const row = db.prepare(`
|
||||
SELECT k.id, k.investisseur_id, k.scopes, k.revoked_at
|
||||
SELECT k.id, k.user_id, k.investisseur_id, k.scope_all, k.scopes, k.revoked_at
|
||||
FROM api_keys k
|
||||
WHERE k.key_hash = ?
|
||||
`).get(hash);
|
||||
@@ -27,7 +34,9 @@ export function requireApiKey(req, res, next) {
|
||||
db.prepare(`UPDATE api_keys SET last_used_at = datetime('now') WHERE id = ?`).run(row.id);
|
||||
|
||||
req.apiKeyId = row.id;
|
||||
req.investisseurId = row.investisseur_id;
|
||||
req.userId = row.user_id;
|
||||
req.investisseurScopeAll = !!row.scope_all;
|
||||
req.investisseurId = req.investisseurScopeAll ? null : row.investisseur_id;
|
||||
req.apiScopes = (row.scopes || 'read').split(',').map(s => s.trim());
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ function generateKey() {
|
||||
/* ── GET /api/api-keys ── liste des clés de l'utilisateur connecté ──────── */
|
||||
router.get('/', (req, res) => {
|
||||
const rows = db.prepare(`
|
||||
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id,
|
||||
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id, k.scope_all,
|
||||
i.nom AS investisseur_nom, k.created_at, k.last_used_at, k.revoked_at
|
||||
FROM api_keys k
|
||||
JOIN investisseurs i ON i.id = k.investisseur_id
|
||||
@@ -27,29 +27,37 @@ router.get('/', (req, res) => {
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
/* ── POST /api/api-keys ── créer une nouvelle clé (nom + investisseur) ──── */
|
||||
/* ── POST /api/api-keys ── créer une nouvelle clé (nom + investisseur, ou
|
||||
scope_all pour "Famille et entreprises") ──────────────────────────────
|
||||
scope_all=true n'est autorisé que si investisseur_id désigne le profil
|
||||
principal — enforcement serveur, indépendant de ce que montre l'UI, pour
|
||||
qu'un appel direct à l'API ne puisse pas contourner cette règle. */
|
||||
router.post('/', (req, res, next) => {
|
||||
try {
|
||||
const nom = (req.body?.nom || '').trim();
|
||||
const investisseur_id = Number(req.body?.investisseur_id);
|
||||
const scope_all = !!req.body?.scope_all;
|
||||
|
||||
if (!nom) throw new HttpError(400, 'Le nom de la clé est requis');
|
||||
if (nom.length > 100) throw new HttpError(400, 'Le nom de la clé est trop long (100 caractères max)');
|
||||
if (!Number.isInteger(investisseur_id)) throw new HttpError(400, 'investisseur_id est requis');
|
||||
|
||||
const inv = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?')
|
||||
const inv = db.prepare('SELECT id, is_principal FROM investisseurs WHERE id = ? AND user_id = ?')
|
||||
.get(investisseur_id, req.user.id);
|
||||
if (!inv) throw new HttpError(404, 'Investisseur introuvable');
|
||||
if (scope_all && !inv.is_principal) {
|
||||
throw new HttpError(403, 'Seul le profil principal peut créer une clé « Famille et entreprises »');
|
||||
}
|
||||
|
||||
const { full, hash, prefix } = generateKey();
|
||||
|
||||
const info = db.prepare(`
|
||||
INSERT INTO api_keys (user_id, investisseur_id, nom, key_prefix, key_hash, scopes)
|
||||
VALUES (?, ?, ?, ?, ?, 'read')
|
||||
`).run(req.user.id, investisseur_id, nom, prefix, hash);
|
||||
INSERT INTO api_keys (user_id, investisseur_id, nom, key_prefix, key_hash, scopes, scope_all)
|
||||
VALUES (?, ?, ?, ?, ?, 'read', ?)
|
||||
`).run(req.user.id, investisseur_id, nom, prefix, hash, scope_all ? 1 : 0);
|
||||
|
||||
const saved = db.prepare(`
|
||||
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id,
|
||||
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id, k.scope_all,
|
||||
i.nom AS investisseur_nom, k.created_at, k.last_used_at, k.revoked_at
|
||||
FROM api_keys k JOIN investisseurs i ON i.id = k.investisseur_id
|
||||
WHERE k.id = ?
|
||||
@@ -74,7 +82,7 @@ router.patch('/:id', (req, res, next) => {
|
||||
db.prepare('UPDATE api_keys SET nom = ? WHERE id = ?').run(nom, req.params.id);
|
||||
|
||||
const saved = db.prepare(`
|
||||
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id,
|
||||
SELECT k.id, k.nom, k.key_prefix, k.scopes, k.investisseur_id, k.scope_all,
|
||||
i.nom AS investisseur_nom, k.created_at, k.last_used_at, k.revoked_at
|
||||
FROM api_keys k JOIN investisseurs i ON i.id = k.investisseur_id
|
||||
WHERE k.id = ?
|
||||
|
||||
@@ -27,9 +27,17 @@ const router = Router();
|
||||
* 200: { description: Synthèse KPI }
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
const invId = req.investisseurId;
|
||||
const annee = req.query.annee ? Number(req.query.annee) : null;
|
||||
|
||||
// Clé "Famille et entreprises" (scope_all) → agrège tous les investisseurs
|
||||
// du foyer (req.userId) ; clé mono-investisseur → filtre sur req.investisseurId.
|
||||
// Dans les deux cas un seul paramètre suffit : soit l'id investisseur, soit
|
||||
// le user_id pour la sous-requête IN (…) — en miroir de ?scope=all côté JWT.
|
||||
const invCond = (col) => req.investisseurScopeAll
|
||||
? `${col} IN (SELECT id FROM investisseurs WHERE user_id = ?)`
|
||||
: `${col} = ?`;
|
||||
const invParam = req.investisseurScopeAll ? req.userId : req.investisseurId;
|
||||
|
||||
// ── Investissements : mêmes formules que le KPI "Capital investi" / "Capital
|
||||
// en risque" de l'app interne (Dashboard.jsx → capitalDeploye = encours +
|
||||
// en_defaut). "capital_investi" et "capital_en_risque" sont des soldes
|
||||
@@ -51,11 +59,11 @@ router.get('/', (req, res) => {
|
||||
- COALESCE((SELECT SUM(rb.capital) FROM remboursements rb WHERE rb.investissement_id = i.id AND rb.type = 'normal'), 0)
|
||||
END), 0) AS capital_en_risque,
|
||||
COALESCE(SUM(CASE WHEN i.statut='rembourse' THEN i.montant_investi END), 0) AS rembourse
|
||||
FROM investissements i WHERE i.investisseur_id = ?
|
||||
`).get(invId);
|
||||
FROM investissements i WHERE ${invCond('i.investisseur_id')}
|
||||
`).get(invParam);
|
||||
|
||||
const interetsConds = ['i.investisseur_id = ?'];
|
||||
const interetsParams = [invId];
|
||||
const interetsConds = [invCond('i.investisseur_id')];
|
||||
const interetsParams = [invParam];
|
||||
if (annee) { interetsConds.push(`strftime('%Y', r.date_remb) = ?`); interetsParams.push(String(annee)); }
|
||||
|
||||
const interets = db.prepare(`
|
||||
@@ -73,8 +81,8 @@ router.get('/', (req, res) => {
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN type='depot' THEN montant END), 0) AS total_depots,
|
||||
COALESCE(SUM(CASE WHEN type='retrait' THEN montant END), 0) AS total_retraits
|
||||
FROM depots_retraits WHERE investisseur_id = ?
|
||||
`).get(invId);
|
||||
FROM depots_retraits WHERE ${invCond('investisseur_id')}
|
||||
`).get(invParam);
|
||||
|
||||
res.json({ investissements, interets: { ...interets, annee: annee || null }, cash });
|
||||
});
|
||||
|
||||
@@ -14,14 +14,21 @@ const router = Router();
|
||||
* 200: { description: Liste des mouvements }
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
// Clé "Famille et entreprises" (scope_all) → tous les investisseurs du
|
||||
// foyer ; clé mono-investisseur → filtre sur req.investisseurId.
|
||||
const invCond = req.investisseurScopeAll
|
||||
? 'dr.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
|
||||
: 'dr.investisseur_id = ?';
|
||||
const invParam = req.investisseurScopeAll ? req.userId : req.investisseurId;
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT dr.id, dr.date_operation, p.nom AS plateforme_nom, dr.type,
|
||||
dr.montant, dr.libelle
|
||||
FROM depots_retraits dr
|
||||
JOIN plateformes p ON p.id = dr.plateforme_id
|
||||
WHERE dr.investisseur_id = ?
|
||||
WHERE ${invCond}
|
||||
ORDER BY dr.date_operation DESC
|
||||
`).all(req.investisseurId);
|
||||
`).all(invParam);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
|
||||
@@ -27,8 +27,12 @@ const LIST_COLUMNS = `
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
const { statut } = req.query;
|
||||
const conds = ['i.investisseur_id = ?'];
|
||||
const args = [req.investisseurId];
|
||||
// Clé "Famille et entreprises" (scope_all) → tous les investisseurs du
|
||||
// foyer ; clé mono-investisseur → filtre sur req.investisseurId.
|
||||
const conds = [req.investisseurScopeAll
|
||||
? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
|
||||
: 'i.investisseur_id = ?'];
|
||||
const args = [req.investisseurScopeAll ? req.userId : req.investisseurId];
|
||||
if (statut) { conds.push('i.statut = ?'); args.push(statut); }
|
||||
|
||||
const rows = db.prepare(`
|
||||
@@ -59,12 +63,17 @@ router.get('/', (req, res) => {
|
||||
*/
|
||||
router.get('/:id', (req, res, next) => {
|
||||
try {
|
||||
const invCond = req.investisseurScopeAll
|
||||
? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
|
||||
: 'i.investisseur_id = ?';
|
||||
const invParam = req.investisseurScopeAll ? req.userId : req.investisseurId;
|
||||
|
||||
const inv = db.prepare(`
|
||||
SELECT ${LIST_COLUMNS}, i.notes
|
||||
FROM investissements i
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
WHERE i.id = ? AND i.investisseur_id = ?
|
||||
`).get(req.params.id, req.investisseurId);
|
||||
WHERE i.id = ? AND ${invCond}
|
||||
`).get(req.params.id, invParam);
|
||||
if (!inv) throw new HttpError(404, 'Investissement introuvable');
|
||||
|
||||
const remboursements = db.prepare(`
|
||||
|
||||
@@ -7,16 +7,30 @@ const router = Router();
|
||||
* @openapi
|
||||
* /investisseur:
|
||||
* get:
|
||||
* summary: Profil de l'investisseur lié à la clé API
|
||||
* summary: Profil investisseur (ou liste, pour une clé "Famille et entreprises")
|
||||
* description: >
|
||||
* Avec une clé scopée à un seul investisseur, renvoie son profil (objet).
|
||||
* Avec une clé "Famille et entreprises" (scope_all), renvoie la liste des
|
||||
* investisseurs du foyer (tableau) — il n'y a plus un profil unique à
|
||||
* renvoyer.
|
||||
* tags: [Investisseur]
|
||||
* security: [{ ApiKeyAuth: [] }]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Profil investisseur
|
||||
* description: Profil investisseur, ou liste de profils si scope_all
|
||||
* 401:
|
||||
* description: Clé API invalide ou manquante
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
if (req.investisseurScopeAll) {
|
||||
const investisseurs = db.prepare(`
|
||||
SELECT id, nom, prenom, type, type_fiscal, notes, created_at
|
||||
FROM investisseurs WHERE user_id = ?
|
||||
ORDER BY is_principal DESC, id ASC
|
||||
`).all(req.userId);
|
||||
return res.json(investisseurs);
|
||||
}
|
||||
|
||||
const inv = db.prepare(`
|
||||
SELECT id, nom, prenom, type, type_fiscal, notes, created_at
|
||||
FROM investisseurs WHERE id = ?
|
||||
|
||||
@@ -22,8 +22,12 @@ const router = Router();
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
const { date_debut, date_fin } = req.query;
|
||||
const conds = ['i.investisseur_id = ?'];
|
||||
const args = [req.investisseurId];
|
||||
// Clé "Famille et entreprises" (scope_all) → tous les investisseurs du
|
||||
// foyer ; clé mono-investisseur → filtre sur req.investisseurId.
|
||||
const conds = [req.investisseurScopeAll
|
||||
? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
|
||||
: 'i.investisseur_id = ?'];
|
||||
const args = [req.investisseurScopeAll ? req.userId : req.investisseurId];
|
||||
if (date_debut) { conds.push('r.date_remb >= ?'); args.push(date_debut); }
|
||||
if (date_fin) { conds.push('r.date_remb <= ?'); args.push(date_fin); }
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ export default function Aide() {
|
||||
<td style={{ padding: '8px 10px 8px 0', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
|
||||
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending_get_investisseur</code>
|
||||
</td>
|
||||
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Profil de l'investisseur lié à la clé API (nom, type famille/entreprise, régime fiscal).</td>
|
||||
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Profil de l'investisseur lié à la clé API (nom, type famille/entreprise, régime fiscal). Avec une clé « Famille et entreprises », renvoie la liste des profils du foyer.</td>
|
||||
</tr>
|
||||
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<td style={{ padding: '8px 10px 8px 0', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
|
||||
|
||||
@@ -805,6 +805,12 @@ function NewApiKeyModal({ open, onClose, onCreated, investisseurs }) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [err, setErr] = useState(null);
|
||||
|
||||
// La clé "Famille et entreprises" (scope_all) n'est autorisée que depuis le
|
||||
// profil principal — cohérent avec l'enforcement côté serveur (routes/apiKeys.js).
|
||||
// Si aucun principal n'est trouvé (ne devrait pas arriver), on masque
|
||||
// simplement l'option plutôt que de proposer un choix qui échouera au submit.
|
||||
const principal = investisseurs.find(i => i.is_principal);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setNom('');
|
||||
@@ -813,6 +819,8 @@ function NewApiKeyModal({ open, onClose, onCreated, investisseurs }) {
|
||||
}
|
||||
}, [open, investisseurs]);
|
||||
|
||||
const isScopeAll = investisseurId === 'all';
|
||||
|
||||
const submit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!nom.trim()) return setErr('Le nom de la clé est requis');
|
||||
@@ -820,7 +828,10 @@ function NewApiKeyModal({ open, onClose, onCreated, investisseurs }) {
|
||||
setBusy(true);
|
||||
setErr(null);
|
||||
try {
|
||||
const created = await api.post('/api-keys', { nom: nom.trim(), investisseur_id: Number(investisseurId) });
|
||||
const payload = isScopeAll
|
||||
? { nom: nom.trim(), investisseur_id: principal.id, scope_all: true }
|
||||
: { nom: nom.trim(), investisseur_id: Number(investisseurId) };
|
||||
const created = await api.post('/api-keys', payload);
|
||||
onCreated(created);
|
||||
} catch (e) { setErr(e.message); }
|
||||
finally { setBusy(false); }
|
||||
@@ -841,10 +852,13 @@ function NewApiKeyModal({ open, onClose, onCreated, investisseurs }) {
|
||||
{investisseurs.map(i => (
|
||||
<option key={i.id} value={i.id}>{memberLabel(i)}</option>
|
||||
))}
|
||||
{principal && <option value="all">Famille et entreprises</option>}
|
||||
</select>
|
||||
</div>
|
||||
<p className="text-muted" style={{ margin: 0, fontSize: 'var(--fs-sm)' }}>
|
||||
La clé donne un accès en lecture seule aux données de cet investisseur. Elle ne sera affichée en clair qu'une seule fois.
|
||||
{isScopeAll
|
||||
? "La clé donnera un accès en lecture seule agrégé à tous les membres du foyer (comme la vue « Famille et entreprises » de l'app). Elle ne sera affichée en clair qu'une seule fois."
|
||||
: "La clé donne un accès en lecture seule aux données de cet investisseur. Elle ne sera affichée en clair qu'une seule fois."}
|
||||
</p>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 4 }}>
|
||||
<button type="button" className="ghost" onClick={onClose}>Annuler</button>
|
||||
@@ -1043,7 +1057,7 @@ function ApiKeysSection() {
|
||||
{keys.map(k => (
|
||||
<tr key={k.id}>
|
||||
<td>{k.nom}</td>
|
||||
<td>{k.investisseur_nom}</td>
|
||||
<td>{k.scope_all ? 'Famille et entreprises' : k.investisseur_nom}</td>
|
||||
<td style={{ fontFamily: 'monospace', fontSize: 12 }}>{k.key_prefix}…</td>
|
||||
<td>{fmtKeyDate(k.created_at)}</td>
|
||||
<td>{fmtKeyDate(k.last_used_at)}</td>
|
||||
@@ -1149,10 +1163,12 @@ function CopyBlock({ text }) {
|
||||
}
|
||||
|
||||
function McpServerSection({ goToApiKeys }) {
|
||||
const navigate = useNavigate();
|
||||
const [mcpUrl, setMcpUrl] = useState(guessMcpUrl());
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [manualLabel, setManualLabel] = useState(null); // null = auto-détecté depuis mcpUrl, sinon override manuel
|
||||
const [debugFlag, setDebugFlag] = useState(false); // ajoute --debug : génère un fichier mcp-server-<nom>.log dédié (voir Dépannage)
|
||||
const [systemCaFlag, setSystemCaFlag] = useState(true); // ajoute NODE_OPTIONS=--use-system-ca : contourne un antivirus/proxy qui intercepte le HTTPS (voir Dépannage) — coché par défaut, ne s'applique qu'en prod (voir envBlock)
|
||||
|
||||
const detectedLabel = detectLabelFromUrl(mcpUrl);
|
||||
const label = manualLabel ?? detectedLabel;
|
||||
@@ -1177,11 +1193,20 @@ function McpServerSection({ goToApiKeys }) {
|
||||
// macOS/Linux, qui n'ont pas ce problème (npx s'exécute nativement).
|
||||
const isWindows = typeof navigator !== 'undefined' && /win/i.test(navigator.platform || navigator.userAgent || '');
|
||||
|
||||
const envBlock = {
|
||||
[envVarName]: apiKey || '<VOTRE_CLE_API>',
|
||||
// Uniquement pertinent pour une URL distante (prod) : force Node à utiliser
|
||||
// le magasin de certificats du système plutôt que le sien, pour accepter
|
||||
// les connexions re-signées par un antivirus/proxy à inspection HTTPS
|
||||
// (Avast, Kaspersky, proxy d'entreprise…) — voir Dépannage.
|
||||
...(!isLocal && systemCaFlag ? { NODE_OPTIONS: '--use-system-ca' } : {}),
|
||||
};
|
||||
|
||||
const configJson = JSON.stringify({
|
||||
mcpServers: {
|
||||
[serverKey]: isWindows
|
||||
? { command: 'cmd', args: ['/c', 'npx', ...mcpRemoteArgs], env: { [envVarName]: apiKey || '<VOTRE_CLE_API>' } }
|
||||
: { command: 'npx', args: mcpRemoteArgs, env: { [envVarName]: apiKey || '<VOTRE_CLE_API>' } },
|
||||
? { command: 'cmd', args: ['/c', 'npx', ...mcpRemoteArgs], env: envBlock }
|
||||
: { command: 'npx', args: mcpRemoteArgs, env: envBlock },
|
||||
},
|
||||
}, null, 2);
|
||||
|
||||
@@ -1261,12 +1286,15 @@ function McpServerSection({ goToApiKeys }) {
|
||||
action nécessaire au-delà de renseigner l'URL et votre clé API.
|
||||
</p>
|
||||
)}
|
||||
{isLocal && (
|
||||
<p style={{ margin: '0 0 20px', fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||
Claude Desktop peut se connecter à plusieurs serveurs MCP en même temps : pour avoir dev et
|
||||
prod accessibles simultanément, répétez ces étapes une deuxième fois avec l'autre URL (et une clé
|
||||
API distincte) — la clé de config (<code>{serverKey}</code> ci-dessous) s'ajuste automatiquement,
|
||||
Claude Desktop ne confondra jamais les deux portefeuilles.
|
||||
Claude Desktop peut se connecter à plusieurs serveurs MCP en même temps : pour avoir aussi votre
|
||||
portefeuille en production accessible depuis Claude Desktop, répétez ces étapes une deuxième fois
|
||||
avec l'URL de production (et une clé API distincte) — la clé de config
|
||||
(<code>{serverKey}</code> ci-dessous) s'ajuste automatiquement, Claude Desktop ne confondra jamais
|
||||
les deux connexions.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<h4 style={{ margin: '0 0 6px', fontSize: 'var(--fs-sm)' }}>3. Copier la configuration</h4>
|
||||
<p style={{ margin: '0 0 10px', fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||
@@ -1295,10 +1323,27 @@ function McpServerSection({ goToApiKeys }) {
|
||||
style={{ width: '100%', maxWidth: 520, borderRadius: 8, border: '1px solid var(--border)', display: 'block', margin: '0 auto 16px' }}
|
||||
onError={(e) => { e.currentTarget.style.display = 'none'; }}
|
||||
/>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, margin: '0 0 10px', fontWeight: 400, fontSize: 'var(--fs-sm)', cursor: 'pointer' }}>
|
||||
<div style={{ margin: '0 0 10px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontWeight: 400, fontSize: 'var(--fs-sm)', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={debugFlag} onChange={e => setDebugFlag(e.target.checked)} style={{ width: 'auto' }} />
|
||||
Ajouter <code>--debug</code> (recommandé en cas de problème — voir Dépannage ci-dessous)
|
||||
Ajouter <code>--debug</code>
|
||||
</label>
|
||||
<p style={{ margin: '2px 0 0 22px', fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||
Recommandé en cas de problème — voir Dépannage ci-dessous.
|
||||
</p>
|
||||
</div>
|
||||
{!isLocal && (
|
||||
<div style={{ margin: '0 0 10px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontWeight: 400, fontSize: 'var(--fs-sm)', cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={systemCaFlag} onChange={e => setSystemCaFlag(e.target.checked)} style={{ width: 'auto' }} />
|
||||
Ajouter <code>NODE_OPTIONS=--use-system-ca</code>
|
||||
</label>
|
||||
<p style={{ margin: '2px 0 0 22px', fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||
Coché par défaut : nécessaire si un antivirus ou un proxy intercepte le HTTPS — voir Dépannage
|
||||
ci-dessous. Décochez uniquement si vous savez que ce n'est pas votre cas.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<CopyBlock text={configJson} />
|
||||
|
||||
<h4 style={{ margin: '20px 0 6px', fontSize: 'var(--fs-sm)' }}>4. Redémarrer Claude Desktop</h4>
|
||||
@@ -1307,12 +1352,22 @@ function McpServerSection({ goToApiKeys }) {
|
||||
</p>
|
||||
|
||||
<h4 style={{ margin: '0 0 6px', fontSize: 'var(--fs-sm)' }}>5. Vérifier</h4>
|
||||
<p style={{ margin: '0 0 20px', fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||
Dans la liste des outils MCP de Claude Desktop, les outils <code>crowdlending_*</code> doivent
|
||||
apparaître (6 en production, 7 en développement local si <code>crowdlending_fetch_url</code> est
|
||||
activé — voir <code>mcp-server/README.md</code>). Testez avec une question du type « Quel est mon
|
||||
encours de crowdlending actuellement ? ».
|
||||
<p style={{ margin: '0 0 10px', fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||
Demandez à Claude : « <strong style={{ color: 'var(--text)' }}>Peux-tu me lister les outils auxquels
|
||||
tu as accès ?</strong> » — c'est la façon la plus fiable de vérifier que la connexion fonctionne
|
||||
vraiment (plutôt qu'une question sur vos données, qui peut échouer pour d'autres raisons même si la
|
||||
connexion est bonne). Les outils <code>crowdlending_*</code> doivent apparaître dans sa réponse
|
||||
{isLocal
|
||||
? <> (6, ou 7 si <code>crowdlending_fetch_url</code> est activé — voir <code>mcp-server/README.md</code>)</>
|
||||
: ' (6 au total)'}.
|
||||
</p>
|
||||
<p style={{ margin: '0 0 20px', fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||
Une fois la connexion confirmée, consultez la FAQ « Comment utiliser le serveur MCP » pour des
|
||||
exemples de questions et le détail de chaque fonction.
|
||||
</p>
|
||||
<button type="button" className="ghost" onClick={() => navigate('/aide?section=faq')} style={{ marginTop: -12, marginBottom: 20 }}>
|
||||
Voir la FAQ →
|
||||
</button>
|
||||
|
||||
<h4 style={{ margin: '0 0 6px', fontSize: 'var(--fs-sm)' }}>6. Dépannage</h4>
|
||||
|
||||
@@ -1341,6 +1396,31 @@ function McpServerSection({ goToApiKeys }) {
|
||||
l'étape 3 (essayez l'autre si l'un des deux est vide ou ne se met pas à jour).
|
||||
</p>
|
||||
|
||||
{!isLocal && (
|
||||
<>
|
||||
<p style={{ margin: '0 0 4px', fontSize: 'var(--fs-sm)', fontWeight: 600 }}>
|
||||
Connexion refusée avec une erreur de certificat, alors que le serveur répond bien
|
||||
</p>
|
||||
<p style={{ margin: '0 0 14px', fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||
Symptôme différent du précédent : la config est correcte, mais <code>mcp-remote</code> échoue à
|
||||
joindre l'URL de production elle-même (pas le registre npm cette fois) avec une erreur
|
||||
<code> UNABLE_TO_VERIFY_LEAF_SIGNATURE</code> dans les logs. Même cause de fond — un antivirus ou
|
||||
un proxy avec inspection HTTPS (Avast, Kaspersky, ESET…) re-signe le trafic avec son propre
|
||||
certificat, que Node.js ne reconnaît pas (contrairement à votre navigateur, qui lui fait
|
||||
confiance via le magasin Windows). Solution : cochez la case <code>NODE_OPTIONS=--use-system-ca</code>
|
||||
ci-dessus (étape 3) — Node utilisera alors le magasin de certificats Windows plutôt que le sien.
|
||||
Nécessite Node.js 22.16 ou plus récent (vérifiable avec <code>node -v</code>) ; sur une version
|
||||
plus ancienne, il faut exporter le certificat racine de l'antivirus (<code>certmgr.msc</code> →
|
||||
Autorités de certification racines de confiance → Exporter en Base-64 X.509) et le référencer via
|
||||
<code> NODE_EXTRA_CA_CERTS</code> à la place. Évitez de désactiver complètement la vérification
|
||||
TLS (<code>NODE_TLS_REJECT_UNAUTHORIZED=0</code>) : cette connexion sort sur internet avec votre
|
||||
clé API et vos données, contrairement au développement local en <code>localhost</code>.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isLocal && (
|
||||
<>
|
||||
<p style={{ margin: '0 0 4px', fontSize: 'var(--fs-sm)', fontWeight: 600 }}>Vérifier côté serveur</p>
|
||||
<p style={{ margin: 0, fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||
La console du serveur (le terminal où tourne <code>npm run dev</code>) affiche désormais une ligne
|
||||
@@ -1348,6 +1428,8 @@ function McpServerSection({ goToApiKeys }) {
|
||||
été fait depuis Claude Desktop, la requête n'arrive jamais jusqu'ici : le problème est côté
|
||||
<code>npx</code>/<code>mcp-remote</code> (voir ci-dessus), pas dans <code>server.js</code>.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ Tous en lecture seule (`readOnlyHint: true`) :
|
||||
|
||||
| Outil | Description |
|
||||
|---|---|
|
||||
| `crowdlending_get_investisseur` | Profil de l'investisseur lié à la clé |
|
||||
| `crowdlending_get_investisseur` | Profil de l'investisseur lié à la clé (liste des membres si clé « Famille et entreprises ») |
|
||||
| `crowdlending_get_dashboard` | Synthèse KPI : capital investi, capital en risque, intérêts perçus (filtrable par année), cash |
|
||||
| `crowdlending_list_investissements` | Liste des investissements, filtrable par statut |
|
||||
| `crowdlending_get_investissement` | Détail d'un investissement + ses remboursements |
|
||||
|
||||
+4
-1
@@ -51,7 +51,10 @@ export function registerDataTools(server, { apiGet, withLabel = (t) => t, withSo
|
||||
description: withSource(
|
||||
"Retourne le profil de l'investisseur associé à la clé API utilisée " +
|
||||
"(nom, type famille/entreprise, régime fiscal). Utile pour savoir sur " +
|
||||
"quel portefeuille portent les autres outils."),
|
||||
"quel portefeuille portent les autres outils. Avec une clé « Famille et " +
|
||||
"entreprises » (scope agrégé), retourne une liste de profils (un par " +
|
||||
"membre du foyer) au lieu d'un profil unique — dans ce cas, les autres " +
|
||||
"outils portent sur l'ensemble des membres, pas un seul."),
|
||||
inputSchema: {},
|
||||
annotations: READ_ONLY_ANNOTATIONS,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user