feat: connecteurs Google + Microsoft 365 (OAuth2 PKCE) — phase 6/8 (v7.56.0)
FlowDeck CI / lint (push) Successful in 2m14s
FlowDeck CI / test (push) Failing after 16m4s
FlowDeck CI / docker (push) Skipped

- app/services/oauth_connectors.py : flow OAuth2 complet PKCE (S256) pour
  2 fournisseurs décrits par 1 dict — Google (Drive/Gmail/Calendar en lecture
  seule) et Microsoft 365 (Graph Files.Read / Mail.Read / Calendars.Read) ;
  begin() = URL d'autorisation + state + code_verifier, complete() = échange du
  code, access_token() = refresh automatique (60 s de marge, refresh_token
  conservé si absent de la réponse), api_get() = path absolu refusé + validation
  SSRF + borne 20 000 car.
- Tokens chiffrés Fernet en réutilisant calendar_sync._encrypt_tokens (zéro
  dépendance) dans la table connector_tokens (migration 33, PK (kind, user_id)).
- 4 routes /api/agent/connectors/oauth/{kind}/… : status, authorize (cookies
  d'état HttpOnly 10 min, retour same-origin validé), callback (GET safe, state
  comparé en temps constant, tokens stockés puis cookies purgés, redirection
  ?oauth=connected / ?oauth_error=), disconnect. OpenAPI 523 chemins.
- Config + .env.example : GOOGLE_CLIENT_ID/SECRET, MS_CLIENT_ID/SECRET (vidés =
  « non configuré »), redirect URI dérivé d'APP_BASE_URL.
- Catalogue : google/ms365 en natifs avec badge connecté/non connecté ;
  connector_fetch et Tester passent par l'API du fournisseur avec le token de
  l'utilisateur (user_id transmis par l'outil LLM).
- Menu + : « Se connecter » / « Déconnecter » sur la fiche, toast au retour du
  flux (URL nettoyée par history.replaceState). État dans la fiche du menu
  plutôt qu'une page dédiée.
- Tests : tests/test_v756_oauth_connectors.py (13), 0 appel réseau réel
  (_post_form / _api_get monkeypatchés) — state forgé refusé sans échange,
  tokens chiffrés en base, refresh, URL absolue refusée, 401/404, câblage menu ;
  test_v755 adapté (5 natifs). Suite complète 1319 verts (-n auto), ruff 0,
  eslint 0 erreur (19 warnings préexistants hors fichiers touchés).
This commit is contained in:
2026-10-07 08:29:22 -04:00
parent 18c72d77fe
commit 1d7750bda0
16 changed files with 983 additions and 33 deletions
+46
View File
@@ -86,6 +86,18 @@
init(){
var self = this;
var el = document.getElementById('fd-agent-panel');
// Retour du flux OAuth (Google / M365) : signaler le résultat puis nettoyer l'URL.
try{
var qs = new URLSearchParams(window.location.search);
if(qs.get('oauth') || qs.get('oauth_error')){
this.toast(qs.get('oauth_error')
? 'Connexion du connecteur refusée : ' + qs.get('oauth_error')
: 'Connecteur connecté.', !!qs.get('oauth_error'));
qs.delete('oauth'); qs.delete('oauth_error');
window.history.replaceState(null, '', window.location.pathname
+ (qs.toString() ? '?' + qs.toString() : '') + window.location.hash);
}
}catch{ /* volontaire */ }
function applyState(){
if(el){ if(self.open){ el.classList.add('open'); el.setAttribute('aria-hidden','false'); } else { el.classList.remove('open'); el.setAttribute('aria-hidden','true'); } }
@@ -1281,6 +1293,14 @@
{key:'cn-probe', icon:'↻', label:'Tester le connecteur',
sub:'Appel de contrôle — status + détail', action:'connector-probe'}
];
if(c.oauth){
var connected = c.status === 'ok';
items.push({key:'cn-oauth', icon: connected ? '🔓' : '🔑',
label: connected ? 'Déconnecter' : 'Se connecter',
sub: connected ? 'Retire les autorisations de ce compte'
: 'Ouvre la page de connexion du fournisseur',
action: connected ? 'connector-disconnect' : 'connector-connect'});
}
if(!c.builtin){
items.push({key:'cn-toggle', icon: c.enabled ? '⏸' : '▶',
label: c.enabled ? 'Désactiver' : 'Activer',
@@ -1319,6 +1339,30 @@
self.fetchConnectors();
}).catch(function(){ self.toast('Bascule impossible (réseau).', true); });
},
// ── Connexion OAuth Google / M365 (v7.56.0) ──
connectorConnect(){
var self = this, c = this.connector;
if(!c || !c.oauth) return;
fetch('/api/agent/connectors/oauth/' + c.kind + '/authorize', {
method:'POST', headers:{'X-CSRF-Token': getCsrf()}
}).then(function(r){ return r.json().then(function(d){ return {ok:r.ok, d:d}; }); })
.then(function(res){
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Connexion impossible.', true); return; }
window.location.href = res.d.url; // → fournisseur → callback → retour ici
}).catch(function(){ self.toast('Connexion impossible (réseau).', true); });
},
connectorDisconnect(){
var self = this, c = this.connector;
if(!c || !c.oauth) return;
fetch('/api/agent/connectors/oauth/' + c.kind + '/disconnect', {
method:'POST', headers:{'X-CSRF-Token': getCsrf()}
}).then(function(r){ return r.json().then(function(d){ return {ok:r.ok, d:d}; }); })
.then(function(res){
if(!res.ok){ self.toast('Déconnexion impossible.', true); return; }
self.toast('Connecteur déconnecté.');
self.fetchConnectors();
}).catch(function(){ self.toast('Déconnexion impossible (réseau).', true); });
},
connectorDelete(){
var self = this, c = this.connector;
if(!c || c.builtin || c.id == null) return;
@@ -1520,6 +1564,8 @@
if(it.action === 'connector-probe'){ this.connectorProbe(); return; }
if(it.action === 'connector-toggle'){ this.connectorToggle(); return; }
if(it.action === 'connector-delete'){ this.connectorDelete(); return; }
if(it.action === 'connector-connect'){ this.connectorConnect(); return; }
if(it.action === 'connector-disconnect'){ this.connectorDisconnect(); return; }
// ── Design System – Canevas (v7.53.0) ──
if(it.action === 'canvases'){
this.fetchCanvases(); this.plusSection = 'canvases'; this.plusFocus = 0; return;