feat: v6.2.0 Web Clipper — extension navigateur (capture article/selection/bookmark/screenshot)
- Service app/services/web_clipper.py: sanitize HTML, html->blocks, extraction article, creation page workspace-aware, rate limit 50/h, device registration - Router app/routers/web_clipper.py: POST /api/v2/web-clipper/clip, GET /status, POST /auth/verify, GET/DELETE /devices, GET /extensions (download page), auth via session ou Bearer (api_tokens / extension_devices) - Migration 19: extension_devices + extension_clips (+ indexes) - Extension Manifest V3: content.js (floating button, selection), background.js (clip + contextMenus), popup.html/js, clipper.css, icons - Settings UI: onglet Extensions (liste devices, revoke, test clip, liens download), page /extensions - Tests: 16 tests web_clipper (sanitize, blocks, article/bookmark/selection/screenshot, bearer, rate-limit, devices, extensions page) - Bump version 6.1.0 -> 6.2.0
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
# V6.0.0 — SSO / SAML : Enterprise Authentication
|
||||
|
||||
> **Statut** : Conception détaillée — v6.0.0
|
||||
> **Date** : 2026-09-15
|
||||
> **Route** : `feat/v6-sso-saml` → `develop` → `main`
|
||||
> **Dépendances** : v4.0.0 Accounts & Integrations (OAuth2 Gitea/GitHub existant)
|
||||
|
||||
---
|
||||
|
||||
## 1. Vision & objectifs
|
||||
|
||||
Ajouter le support **SSO/SAML 2.0** pour permettre aux entreprises d'intégrer FlowDeck dans leur infrastructure d'authentification existante. Les utilisateurs d'une organisation peuvent se connecter via leur fournisseur d'identité (IdP) d'entreprise sans avoir de compte local séparé.
|
||||
|
||||
### Objectifs
|
||||
|
||||
| Critère | Cible |
|
||||
|---------|-------|
|
||||
| Protocoles | SAML 2.0 (principal), OIDC (complémentaire) |
|
||||
| Fournisseurs supportés | Azure AD, Okta, Google Workspace, OneLogin, Keycloak, Auth0 |
|
||||
| Temps de connexion SSO | < 3 s (redirect + callback) |
|
||||
| Gestion des utilisateurs | Auto-provisioning à la première connexion |
|
||||
| Déconnexion | SLO (Single Logout) supporté |
|
||||
| Fallback | Login local toujours disponible (admin activé/désactivable) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture SSO/SAML
|
||||
|
||||
### 2.1 Flux SAML 2.0
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Utilisateur │ │ FlowDeck │ │ IdP │
|
||||
│ (navigateur) │ │ (SP) │ │ (Azure AD / │
|
||||
│ │ │ │ │ Okta / etc.)│
|
||||
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
||||
│ │ │
|
||||
│ 1. Accède à FlowDeck │ │
|
||||
│───────────────────────►│ │
|
||||
│ │ │
|
||||
│ 2. Redirige vers IdP │ │
|
||||
│◄───────────────────────│ │
|
||||
│ │ │
|
||||
│ 3. SSO redirect │ │
|
||||
│───────────────────────►│────────────────────────►│
|
||||
│ │ │
|
||||
│ 5. Callback avec │ │
|
||||
│ SAMLResponse │◄────────────────────────│
|
||||
│ │ │
|
||||
│ 6. POST /auth/saml/callback │
|
||||
│───────────────────────►│ │
|
||||
│ │ │
|
||||
│ 7. Validate SAML │ │
|
||||
│ → Create/Update │ │
|
||||
│ user → Session │ │
|
||||
│◄───────────────────────│ │
|
||||
│ │ │
|
||||
│ 8. Authentifié │ │
|
||||
│◄───────────────────────│ │
|
||||
```
|
||||
|
||||
### 2.2 Composants côté serveur
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────┐
|
||||
│ FASTAPI (Python) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ auth/routers/sso.py │ │
|
||||
│ │ ├─ GET /auth/saml/login — initie la requête │ │
|
||||
│ │ ├─ POST /auth/saml/callback — traite la réponse │ │
|
||||
│ │ ├─ GET /auth/saml/metadata — SP metadata endpoint │ │
|
||||
│ │ ├─ GET /auth/oidc/login — flux OIDC │ │
|
||||
│ │ ├─ POST /auth/oidc/callback │ │
|
||||
│ │ └─ POST /auth/sso/logout — SLO │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ auth/providers/saml_provider.py │ │
|
||||
│ │ ├─ SAMLProvider — wrapper python3-saml │ │
|
||||
│ │ └─ validate_saml_response() │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ auth/providers/oidc_provider.py │ │
|
||||
│ │ ├─ OIDCProvider — wrapper httpx + OIDC lib │ │
|
||||
│ │ └─ validate_oidc_token() │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ auth/session.py — SessionManager étendu │ │
|
||||
│ │ ├─ create_sso_session() │ │
|
||||
│ │ ├─ handle_sso_user() │ │
|
||||
│ │ └─ enforce_sso_restriction() │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ services/sso_provisioning.py │ │
|
||||
│ │ ├─ auto_provision_user() │ │
|
||||
│ │ ├─ sync_user_attributes() │ │
|
||||
│ │ └─ map_sso_groups_to_workspaces() │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Configuration SAML
|
||||
|
||||
### 3.1 Nouveau table `sso_config`
|
||||
|
||||
```sql
|
||||
CREATE TABLE sso_config (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
workspace_id INTEGER REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
-- NULL = workspace-level; specific workspace; global if NULL+global flag
|
||||
provider_type TEXT NOT NULL,
|
||||
-- 'saml', 'oidc'
|
||||
entity_id TEXT NOT NULL,
|
||||
-- SAML: Entity ID of the IdP (e.g., "http://www.microsoft.com/...")
|
||||
sso_url TEXT NOT NULL,
|
||||
-- SAML: IdP SSO URL (HTTP-POST binding)
|
||||
slo_url TEXT,
|
||||
-- SAML: IdP Single Logout URL
|
||||
x509_certificate TEXT NOT NULL,
|
||||
-- SAML: IdP signing certificate (PEM format)
|
||||
-- OIDC: issuer URL
|
||||
issuer_url TEXT,
|
||||
-- OIDC: OIDC issuer identifier
|
||||
client_id TEXT,
|
||||
-- OIDC: client_id
|
||||
client_secret TEXT,
|
||||
-- OIDC: client_secret (encrypted at rest)
|
||||
scope TEXT DEFAULT 'openid profile email',
|
||||
-- OIDC: scopes requested
|
||||
attribute_mapping TEXT NOT NULL DEFAULT '{}',
|
||||
-- JSON: {"email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", ...}
|
||||
-- Maps SAML/OIDC attributes to FlowDeck user fields
|
||||
auto_provision BOOLEAN NOT NULL DEFAULT 1,
|
||||
-- Auto-create user on first SSO login
|
||||
default_workspace_id INTEGER,
|
||||
-- Workspace to assign new SSO users
|
||||
groups_mapping TEXT DEFAULT '[]',
|
||||
-- JSON: [{"sso_group": "Admins", "workspace_role": "admin"}, ...]
|
||||
active BOOLEAN NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INTEGER REFERENCES users(id)
|
||||
);
|
||||
```
|
||||
|
||||
### 3.2 Mapping des attributs
|
||||
|
||||
Par défaut pour SAML (mappings standard OASIS) :
|
||||
|
||||
```json
|
||||
{
|
||||
"login": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
"email": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
"full_name": "urn:oasis:names:tc:SAML:attribute:displayName",
|
||||
"avatar_url": "urn:oasis:names:tc:SAML:attribute:thumbnail"
|
||||
}
|
||||
```
|
||||
|
||||
Par défaut pour OIDC :
|
||||
|
||||
```json
|
||||
{
|
||||
"login": "sub",
|
||||
"email": "email",
|
||||
"full_name": "name",
|
||||
"avatar_url": "picture"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Configuration `.env` (exemples)
|
||||
|
||||
```env
|
||||
# SAML — Azure AD
|
||||
SSO_PROVIDER=saml
|
||||
SSO_ENTITY_ID=https://sts.windows.net/{tenant-id}/
|
||||
SSO_SSO_URL=https://login.microsoftonline.com/{tenant-id}/saml2
|
||||
SSO_SLO_URL=https://login.microsoftonline.com/{tenant-id}/saml2/logout
|
||||
SSO_X509_CERT="-----BEGIN CERTIFICATE-----\n..."
|
||||
SSO_ATTRIBUTE_MAPPING={"login":"nameid","email":"email","full_name":"name"}
|
||||
SSO_AUTO_PROVISION=true
|
||||
SSO_DEFAULT_WORKSPACE_ID=1
|
||||
SSO_GROUPS_MAPPING=[{"sso_group":"FlowDeck Admins","workspace_role":"admin"}]
|
||||
|
||||
# OIDC — Google Workspace
|
||||
SSO_PROVIDER=oidc
|
||||
SSO_ISSUER_URL=https://accounts.google.com
|
||||
SSO_CLIENT_ID=xxx.apps.googleusercontent.com
|
||||
SSO_CLIENT_SECRET=yyy
|
||||
SSO_SCOPE=openid profile email
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Endpoints API
|
||||
|
||||
### 4.1 Routeur `app/routers/sso.py`
|
||||
|
||||
```
|
||||
GET /auth/saml/login — Redirige vers l'IdP SAML
|
||||
POST /auth/saml/callback — Traite le SAMLResponse, crée la session
|
||||
GET /auth/saml/metadata — Retourne le SP metadata XML (pour configurer l'IdP)
|
||||
POST /auth/saml/logout — Initie le SLO (Single Logout)
|
||||
GET /auth/oidc/login — Redirige vers l'IdP OIDC
|
||||
POST /auth/oidc/callback — Traite le token OIDC, crée la session
|
||||
POST /auth/oidc/logout — SLO OIDC
|
||||
POST /api/v2/sso/config — Créer/configurer SSO (admin, scope admin)
|
||||
GET /api/v2/sso/config — Lire la config SSO courante
|
||||
PUT /api/v2/sso/config — Mettre à jour la config
|
||||
DELETE /api/v2/sso/config — Supprimer la config SSO (désactiver SSO)
|
||||
GET /api/v2/sso/workspaces — Lister les workspaces avec SSO actif
|
||||
POST /api/v2/sso/sync — Forcer la synchro des groupes/attributs
|
||||
```
|
||||
|
||||
### 4.2 Service `app/services/sso_provisioning.py`
|
||||
|
||||
```python
|
||||
class SSOService:
|
||||
"""Provisioning et gestion des utilisateurs SSO."""
|
||||
|
||||
def handle_sso_login(self, sso_data: dict, provider_type: str) -> dict:
|
||||
"""Traite un login SSO : trouve/crée l'utilisateur, crée la session."""
|
||||
# 1. Extraire les attributs selon le provider
|
||||
# 2. Chercher l'utilisateur par email/login SSO
|
||||
# 3. Si trouvé → mise à jour des attributs
|
||||
# 4. Si non trouvé et auto_provision → création
|
||||
# 5. Assigner le workspace par défaut ou le premier disponible
|
||||
# 6. Vérifier les groupes SSO → rôle workspace
|
||||
# 7. Créer la session
|
||||
|
||||
def sync_sso_groups(self, user_id: int, sso_groups: list[str]) -> None:
|
||||
"""Sync les groupes SSO vers les rôles workspace."""
|
||||
# Comparer sso_groups avec groups_mapping
|
||||
# Mettre à jour workspace_members.role
|
||||
|
||||
def enforce_sso_restriction(self, workspace_id: int) -> bool:
|
||||
"""Vérifie si le workspace est en mode SSO-only."""
|
||||
# Si config.sso_only = True → interdire le login local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Sécurité
|
||||
|
||||
### 5.1 Validation des assertions SAML
|
||||
|
||||
| Validation | Détail |
|
||||
|------------|--------|
|
||||
| Signature | Vérifier la signature avec le certificat IdP |
|
||||
| `NotBefore` / `NotOnOrAfter` | Rejeter les assertions expirées |
|
||||
| `Audience` | Vérifier que l'audience match l'entity_id du SP |
|
||||
| `Destination` | Vérifier que le destination match notre callback URL |
|
||||
| `Issuer` | Vérifier que l'émetteur est le IdP attendu |
|
||||
| `InResponseTo` | Prévenir le replay attack |
|
||||
|
||||
### 5.2 Protection supplémentaire
|
||||
|
||||
- **CSRF sur le callback** : le `RelayState` contient un token CSRF validé
|
||||
- **Rate limiting** sur les endpoints SSO : max 5 tentatives/minute
|
||||
- **Logging audit** : chaque login SSO est enregistré (user, provider, IP, timestamp)
|
||||
- **Session fixation** : nouvelle session créée après chaque SSO login
|
||||
- **Encryption** : `client_secret` chiffré avec `app_secret_key` avant stockage
|
||||
|
||||
### 5.3 Table `sso_login_history`
|
||||
|
||||
```sql
|
||||
CREATE TABLE sso_login_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider_type TEXT NOT NULL,
|
||||
provider_name TEXT NOT NULL,
|
||||
sso_identifier TEXT, -- email ou subject du SSO
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
success BOOLEAN NOT NULL,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sso_history_user ON sso_login_history(user_id, created_at);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Interface utilisateur
|
||||
|
||||
### 6.1 Login page — ajout SSO buttons
|
||||
|
||||
```html
|
||||
<!-- Extension de login.html — boutons SSO -->
|
||||
<div class="sso-providers" x-show="ssoProviders.length > 0">
|
||||
<div class="sso-divider">ou continuez avec</div>
|
||||
<template x-for="provider in ssoProviders">
|
||||
<button @click="ssoLogin(provider.type)" class="sso-btn">
|
||||
<span x-text="provider.icon"></span>
|
||||
<span x-text="provider.name"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 6.2 Settings — Configuration SSO (admin)
|
||||
|
||||
Nouvelle section dans `settings.html` → onglet **"SSO / Enterprise"** :
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ SSO / Enterprise Authentication │
|
||||
│───────────────────────────────────────────────────────│
|
||||
│ Provider: [▼ SAML / OIDC] │
|
||||
│ │
|
||||
│ SAML Configuration: │
|
||||
│ ├── Entity ID: [___________________________] │
|
||||
│ ├── SSO URL: [___________________________] │
|
||||
│ ├── SLO URL: [___________________________] │
|
||||
│ ├── Certificate: [text area - PEM] │
|
||||
│ └── Attribute Mapping: [JSON editor] │
|
||||
│ │
|
||||
│ OIDC Configuration: │
|
||||
│ ├── Issuer URL: [_________________________] │
|
||||
│ ├── Client ID: [_________________________] │
|
||||
│ ├── Client Secret: [_______________________] │
|
||||
│ └── Scope: [openid profile email] │
|
||||
│ │
|
||||
│ User Provisioning: │
|
||||
│ ├── Auto-provision new users [✓] │
|
||||
│ ├── Default workspace: [dropdown] │
|
||||
│ └── Group mapping: [table editor] │
|
||||
│ │
|
||||
│ ┌─ Group Mappings ───────────────────────────────────┐ │
|
||||
│ │ SSO Group │ FlowDeck Role │ Workspace │ │
|
||||
│ │ FlowDeck Admins │ admin │ Main │ │
|
||||
│ │ FlowDeck Members │ editor │ Main │ │
|
||||
│ └────────────────────┴────────────────┴──────────────┘ │
|
||||
│ │
|
||||
│ [Save SSO Configuration] [Disable SSO] │
|
||||
│ Status: ✅ Active — 12 users provisioned via SSO │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.3 Métadonnées SP pour l'IdP
|
||||
|
||||
Endpoint `GET /auth/saml/metadata` retourne le XML SAML metadata :
|
||||
|
||||
```xml
|
||||
<EntityDescriptor entityID="https://flowdeck.local/saml/metadata">
|
||||
<SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<KeyDescriptor use="signing">
|
||||
<KeyInfo>...</KeyInfo>
|
||||
</KeyDescriptor>
|
||||
<AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||
Location="https://flowdeck.local/auth/saml/callback" />
|
||||
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
Location="https://flowdeck.local/auth/saml/logout" />
|
||||
</SPSSODescriptor>
|
||||
</EntityDescriptor>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Intégration avec le système existant
|
||||
|
||||
### 7.1 Comportement selon le type de compte
|
||||
|
||||
```
|
||||
SSO ACTIVÉ + local auth ENABLED:
|
||||
→ Boutons SSO visibles sur la login page
|
||||
→ Login local disponible (optionnel, admin-toggle)
|
||||
→ Les utilisateurs SSO et locaux coexistent
|
||||
|
||||
SSO ACTIVÉ + local auth DISABLED:
|
||||
→ Seuls les boutons SSO sont affichés
|
||||
→ Login local caché / 403 pour /auth/local-login
|
||||
→ Admin du workspace peut garder le sien
|
||||
|
||||
SSO ACTIVÉ + compte local existant:
|
||||
→ Email match → fusion automatique des sessions
|
||||
→ Email ne match pas → login local séparé (deux comptes)
|
||||
```
|
||||
|
||||
### 7.2 Interaction avec le PermissionManager
|
||||
|
||||
Le `PermissionManager` existant (`app/services/permission_manager.py`) est étendu :
|
||||
|
||||
```python
|
||||
class PermissionManager:
|
||||
# Méthodes existantes conservées
|
||||
# + Nouvelles méthodes SSO :
|
||||
|
||||
def is_sso_only_workspace(self, workspace_id: int) -> bool:
|
||||
"""Vérifie si le workspace exige SSO."""
|
||||
|
||||
def get_sso_roles(self, user_id: int, workspace_id: int) -> list[str]:
|
||||
"""Retourne les rôles issus des groupes SSO."""
|
||||
|
||||
def sync_sso_permissions(self, user_id: int, sso_groups: list[str], workspace_id: int):
|
||||
"""Met à jour les rôles basés sur les groupes SSO."""
|
||||
```
|
||||
|
||||
### 7.3 Cookie de session SSO
|
||||
|
||||
Le cookie `flowdeck_session` existant est utilisé tel quel. Une colonne supplémentaire dans la table `users` distingue les utilisateurs SSO :
|
||||
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN auth_method TEXT DEFAULT 'local';
|
||||
-- Valeurs: 'local', 'gitea', 'github', 'saml', 'oidc'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Tests
|
||||
|
||||
| Test | Description | Outil |
|
||||
|------|-------------|-------|
|
||||
| SAML login flow | Redirection complète IdP → callback → session | Playwright + mock IdP |
|
||||
| SAML metadata | Endpoint retourne XML valide | Unit test |
|
||||
| SAML assertion validation | Signature invalide → rejet | Unit test |
|
||||
| SAML replay attack | Même assertion utilisée 2× → rejet | Unit test |
|
||||
| OIDC login flow | Authorization code → token → session | Playwright + mock OIDC |
|
||||
| Auto-provision | Nouveau user SSO → création auto | Unit test |
|
||||
| Group mapping | Groupes SSO → rôles workspace | Unit test |
|
||||
| SSO disable | Désactiver SSO → tous les users restants gardent accès | Unit test |
|
||||
| SLO | Logout → redirection IdP SLO | Playwright |
|
||||
| Mixed auth | Login SSO + login local dans le même workspace | Integration test |
|
||||
|
||||
---
|
||||
|
||||
## 9. Checklist d'implémentation
|
||||
|
||||
1. **`auth/providers/saml_provider.py`** — wrapper python3-saml ou pysaml2
|
||||
2. **`auth/providers/oidc_provider.py`** — wrapper OIDC (authlib ou httpx)
|
||||
3. **`auth/routers/sso.py`** — endpoints SSO/OIDC
|
||||
4. **Migration `sso_config`** + `sso_login_history` + colonne `auth_method` sur `users`
|
||||
5. **`services/sso_provisioning.py`** — auto-provision + group mapping
|
||||
6. **Extension `settings.html`** — UI admin SSO
|
||||
7. **Extension `login.html`** — boutons SSO
|
||||
8. **SP metadata endpoint** (`/auth/saml/metadata`)
|
||||
9. **Security** — validation assertions, rate limiting, audit log
|
||||
10. **Tests** — tous les scénarios SSO
|
||||
11. **Documentation utilisateur** — `/help` section SSO setup
|
||||
12. **Dépendance** — `python3-saml` ou `pysaml2`, `authlib` dans requirements.txt
|
||||
|
||||
---
|
||||
|
||||
## 10. Dépendances Python
|
||||
|
||||
```
|
||||
# requirements.txt additions pour v6.0.0 SSO
|
||||
python3-saml>=1.16.0 # SAML 2.0 SP
|
||||
authlib>=1.3.0 # OIDC client
|
||||
cryptography>=42.0 # Signature/encryption
|
||||
xmlsec>=1.3.0 # XML signature validation (optionnel)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Références
|
||||
|
||||
- [OASIS SAML 2.0 Core Specification](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf)
|
||||
- [SAML 2.0 for Dummies (Simplified)](https://www.onelogin.com/sites/default/files/resources/SAML_2.0_for_Dummies.pdf)
|
||||
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
|
||||
- [Azure AD SAML integration](https://learn.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-saml-idp)
|
||||
- [Authlib — OIDC client](https://docs.authlib.org/en/latest/client/oidc.html)
|
||||
- `app/auth/providers.py` — Architecture provider existante
|
||||
- `app/services/permission_manager.py` — Extension des rôles
|
||||
- `docs/API_GUIDE_V6.md` — Référence API v2
|
||||
- `ROADMAP.md` — v6.0.0 SSO/SAML item
|
||||
@@ -0,0 +1,589 @@
|
||||
# V6.0.0 — Web Clipper : Extension Navigateur
|
||||
|
||||
> **Statut** : Conception détaillée — v6.0.0
|
||||
> **Date** : 2026-09-15
|
||||
> **Route** : `feat/v6-web-clipper` → `develop` → `main`
|
||||
> **Dépendances** : v4.0.0 Share & Publish, v5.5.0 Embeds & Rich Media, v5.2.0 OAuth (GitHub)
|
||||
|
||||
---
|
||||
|
||||
## 1. Vision & objectifs
|
||||
|
||||
Créer une **extension de navigateur** qui permet aux utilisateurs de capturer du contenu web (articles, pages, images, bookmarks) directement dans FlowDeck. Le Web Clipper agit comme un pont entre le web et FlowDeck, transformant n'importe quelle page web en une page FlowDeck.
|
||||
|
||||
### Objectifs
|
||||
|
||||
| Critère | Cible |
|
||||
|---------|-------|
|
||||
| Navigateurs supportés | Chrome, Firefox, Edge, Safari |
|
||||
| Types de capture | Article complet, sélection, bookmark, screenshot |
|
||||
| Temps de capture | < 2 s (page simple), < 5 s (page complexe) |
|
||||
| Authentification | OAuth via popup (sans mot de passe) |
|
||||
| Format d'import | Markdown + liens + images inline |
|
||||
| Compatibilité | Fonctionne même si FlowDeck est fermé |
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture
|
||||
|
||||
### 2.1 Composants
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ NAVIGATEUR │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ EXTENSION (Manifest V3) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │ │
|
||||
│ │ │ Content │ │ Popup UI │ │ Background │ │ │
|
||||
│ │ │ Script │ │ (sidebar │ │ Script │ │ │
|
||||
│ │ │ (clipping) │ │ panel) │ │ (OAuth + sync) │ │ │
|
||||
│ │ └──────┬──────┘ └──────┬──────┘ └────────┬─────────┘ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ └────────┬───────┘ │ │ │
|
||||
│ │ ▼ │ │ │
|
||||
│ │ ┌────────────────┐ │ │ │
|
||||
│ │ │ Content │ │ │ │
|
||||
│ │ │ Extractor │ │ │ │
|
||||
│ │ │ (Readability) │ │ │ │
|
||||
│ │ └────────┬───────┘ │ │ │
|
||||
│ └─────────────────┼──────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ POST /api/v2/web-clipper/clip │ │
|
||||
└────────────────────┼─────────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────▼─────────────────────────────────────────┐
|
||||
│ FASTAPI (Server) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ routers/web_clipper.py │ │
|
||||
│ │ ├─ POST /api/v2/web-clipper/clip — recevoir la clip │ │
|
||||
│ │ ├─ GET /api/v2/web-clipper/status — stat OAuth │ │
|
||||
│ │ └─ POST /api/v2/web-clipper/auth/verify — vérifier │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ services/web_clipper.py │ │
|
||||
│ │ ├─ extract_article() — Readability.js / Mercury Parser │ │
|
||||
│ │ ├─ extract_selection() — sélection HTML → Markdown │ │
|
||||
│ │ ├─ create_page_from_clip() — créer la page FlowDeck │ │
|
||||
│ │ ├─ download_images() — télécharger les images inline │ │
|
||||
│ │ └─ generate_thumbnail() — preview image │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Manifest V3 de l'extension
|
||||
|
||||
### 3.1 `manifest.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "FlowDeck Web Clipper",
|
||||
"description": "Capturez du contenu web directement dans FlowDeck",
|
||||
"version": "1.0.0",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"scripting",
|
||||
"contextMenus"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>",
|
||||
"https://flowdeck.local/*",
|
||||
"https://flowdeck.dracodev.net/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "icons/icon-16.png",
|
||||
"32": "icons/icon-32.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"128": "icons/icon-128.png"
|
||||
}
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"],
|
||||
"css": ["clipper.css"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["reader-mode.js", "readability.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
"icons": {
|
||||
"16": "icons/icon-16.png",
|
||||
"32": "icons/icon-32.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"128": "icons/icon-128.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Endpoints serveur
|
||||
|
||||
### 4.1 Routeur `app/routers/web_clipper.py`
|
||||
|
||||
```
|
||||
POST /api/v2/web-clipper/clip — Recevoir une capture et créer une page
|
||||
GET /api/v2/web-clipper/status — Vérifier l'authentification de l'extension
|
||||
POST /api/v2/web-clipper/auth/verify — Vérifier le token OAuth de l'extension
|
||||
POST /api/v2/web-clipper/auth/callback — OAuth callback pour l'extension
|
||||
```
|
||||
|
||||
### 4.2 Service `app/services/web_clipper.py`
|
||||
|
||||
```python
|
||||
class WebClipperService:
|
||||
"""Traitement des captures web."""
|
||||
|
||||
async def process_clip(self, clip_data: ClipPayload, user_id: int) -> dict:
|
||||
"""Traite une capture complète et crée la page FlowDeck.
|
||||
|
||||
ClipPayload:
|
||||
- url: str (URL source)
|
||||
- title: str (titre extrait)
|
||||
- content: str (HTML ou Markdown)
|
||||
- content_type: str ('article' | 'selection' | 'bookmark' | 'screenshot')
|
||||
- images: list[dict] (URLs + base64 data)
|
||||
- metadata: dict (og:title, og:description, author, date_published)
|
||||
- workspace_id: int | None
|
||||
- collection_id: int | None
|
||||
- parent_page_id: int | None
|
||||
"""
|
||||
|
||||
async def extract_article(self, html: str, url: str) -> dict:
|
||||
"""Extrait le contenu principal d'un article via Readability.js."""
|
||||
# Retourne: {title, content (HTML), text_content (Markdown), images[]}
|
||||
|
||||
async def extract_selection(self, html: str, selection_html: str) -> dict:
|
||||
"""Extrait la sélection de texte de la page."""
|
||||
# Retourne: {content: HTML, text_content: Markdown}
|
||||
|
||||
async def create_page_from_clip(self, clip_data: dict, user_id: int) -> dict:
|
||||
"""Crée une page FlowDeck à partir d'une capture.
|
||||
Inclut la création d'images inline (upload vers /api/upload)."""
|
||||
|
||||
async def download_images(self, image_urls: list[str]) -> list[str]:
|
||||
"""Télécharge les images et retourne les URLs locales."""
|
||||
```
|
||||
|
||||
### 4.3 Payload de la capture
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://example.com/article",
|
||||
"title": "Titre de l'article",
|
||||
"content": "<article>...contenu HTML...</article>",
|
||||
"content_type": "article",
|
||||
"content_format": "html",
|
||||
"images": [
|
||||
{"src": "https://example.com/image1.jpg", "alt": "Description", "base64": null},
|
||||
{"src": "https://example.com/image2.png", "alt": null, "base64": "data:image/png;base64,..."}
|
||||
],
|
||||
"metadata": {
|
||||
"og_title": "Titre Open Graph",
|
||||
"og_description": "Description de l'article",
|
||||
"author": "Auteur",
|
||||
"date_published": "2026-09-01",
|
||||
"site_name": "Example.com",
|
||||
"favicon_url": "https://example.com/favicon.ico"
|
||||
},
|
||||
"target_workspace_id": 42,
|
||||
"target_collection_id": null,
|
||||
"target_page_id": null,
|
||||
"tags": ["lecture", "important"],
|
||||
"create_as_draft": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Contenu de l'extension côté client
|
||||
|
||||
### 5.1 Content Script — Extraction du contenu
|
||||
|
||||
```javascript
|
||||
// content.js — injecté dans toutes les pages
|
||||
|
||||
class FlowDeckClipper {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 1. Bouton flottant "Clip to FlowDeck"
|
||||
this.createFloatButton();
|
||||
|
||||
// 2. Menu contextuel (clic droit)
|
||||
this.createContextMenu();
|
||||
|
||||
// 3. Keyboard shortcut (Ctrl+Shift+C)
|
||||
this.registerShortcut();
|
||||
}
|
||||
|
||||
createFloatButton() {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'fd-clipper-btn';
|
||||
btn.innerHTML = '📌 Clip to FlowDeck';
|
||||
btn.addEventListener('click', () => this.openClipper());
|
||||
document.body.appendChild(btn);
|
||||
}
|
||||
|
||||
async openClipper() {
|
||||
const url = window.location.href;
|
||||
const html = document.documentElement.outerHTML;
|
||||
const selection = window.getSelection().toString();
|
||||
|
||||
// Envoyer au service worker
|
||||
const clipData = { url, html, selection, title: document.title };
|
||||
await chrome.runtime.sendMessage({ action: 'clip', data: clipData });
|
||||
}
|
||||
|
||||
createContextMenu() {
|
||||
// Cliquez sur une sélection → menu contextuel "Send to FlowDeck"
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
const selection = window.getSelection().toString();
|
||||
if (selection.length > 0) {
|
||||
const menuItem = document.createElement('div');
|
||||
menuItem.className = 'fd-context-menu';
|
||||
menuItem.innerHTML = '<span>📌 Clip selection to FlowDeck</span>';
|
||||
menuItem.addEventListener('click', () => this.clipSelection(selection));
|
||||
document.body.appendChild(menuItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Content Extractor — Readability.js
|
||||
|
||||
L'extension embarque une version simplifiée de **Readability.js** (Mozilla) pour extraire le contenu principal des articles :
|
||||
|
||||
```javascript
|
||||
// reader-mode.js — extraction du contenu principal
|
||||
// Version embarquée simplifiée basée sur l'algorithme Readability
|
||||
|
||||
function extractArticle(html) {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
// Algorithme : trouver le meilleur candidat basé sur la longueur de texte,
|
||||
// les balises <article>, <main>, <div role="article">, etc.
|
||||
// Retourne: { title, content, textContent }
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Background Script — OAuth et Sync
|
||||
|
||||
```javascript
|
||||
// background.js — Service worker de l'extension
|
||||
|
||||
// Stockage local des tokens et config
|
||||
const STORAGE_KEY = 'flowdeck_clipper';
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.storage.local.set({ [STORAGE_KEY]: { authenticated: false } });
|
||||
});
|
||||
|
||||
// ── Gestion des messages ──
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.action === 'clip') {
|
||||
handleClip(message.data).then(result => sendResponse(result));
|
||||
return true; // async
|
||||
}
|
||||
if (message.action === 'auth') {
|
||||
handleAuth().then(result => sendResponse(result));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Clip → Serveur ──
|
||||
async function handleClip(data) {
|
||||
const token = await getToken();
|
||||
if (!token || !isAuthenticated()) {
|
||||
return { error: 'not_authenticated', requiresAuth: true };
|
||||
}
|
||||
|
||||
const response = await fetch('https://flowdeck.local/api/v2/web-clipper/clip', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// ── OAuth Flow ──
|
||||
async function handleAuth() {
|
||||
// Popup OAuth → redirect to /auth/sso/callback → receive token
|
||||
// Stocker le token dans chrome.storage.local
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Authentification de l'extension
|
||||
|
||||
### 6.1 OAuth Device Flow ou Popup
|
||||
|
||||
L'extension utilise le même OAuth2 que le web :
|
||||
|
||||
```
|
||||
Extension → Background Script → Popup Auth
|
||||
1. User clique "Connect"
|
||||
2. Popup opens → GET /auth/login (avec redirect vers extension)
|
||||
3. OAuth callback → token reçu
|
||||
4. Token stocké dans chrome.storage.local (encrypted)
|
||||
5. Tous les clips sont authentifiés automatiquement
|
||||
```
|
||||
|
||||
### 6.2 Token storage
|
||||
|
||||
```python
|
||||
# Côté serveur : table pour les tokens d'extension
|
||||
ALTER TABLE api_tokens ADD COLUMN source TEXT DEFAULT 'web';
|
||||
-- Valeurs: 'web', 'clipper', 'mobile', 'api'
|
||||
-- Permet de révoquer uniquement les tokens d'extension
|
||||
```
|
||||
|
||||
```sql
|
||||
CREATE TABLE extension_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
device_id TEXT NOT NULL, -- ID unique de l'extension installée
|
||||
token_hash TEXT NOT NULL,
|
||||
scopes TEXT DEFAULT 'read,write',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_used_at TIMESTAMP,
|
||||
revoked INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(user_id, device_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Types de capture supportés
|
||||
|
||||
### 7.1 Article complet
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ URL: https://example.com/article │
|
||||
│ │
|
||||
│ ┌─── Article Content ────────────┐ │
|
||||
│ │ Title extracted │ │
|
||||
│ │ Author: John Doe │ │
|
||||
│ │ Date: 2026-09-15 │ │
|
||||
│ │ │ │
|
||||
│ │ Main article text... │ │
|
||||
│ │ │ │
|
||||
│ │ ![#image1.jpg] │ │
|
||||
│ │ Caption from article │ │
|
||||
│ │ │ │
|
||||
│ │ ![#image2.png] │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Tags: [lecture, tech] │
|
||||
│ Target: Workspace X → Collection Y │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 7.2 Sélection de texte
|
||||
|
||||
```
|
||||
L'utilisateur sélectionne du texte sur une page → clic droit → "Clip to FlowDeck"
|
||||
→ Crée une page FlowDeck avec le texte sélectionné formaté en Markdown
|
||||
→ Ajoute un lien vers la page source en bas
|
||||
```
|
||||
|
||||
### 7.3 Bookmark
|
||||
|
||||
```
|
||||
L'utilisateur clique sur le bouton clipper sur une page sans contenu riche
|
||||
→ Crée un "bookmark card" (comme v5.5.0 bookmark cards)
|
||||
→ URL, titre, favicon, description OG → page FlowDeck
|
||||
```
|
||||
|
||||
### 7.4 Screenshot
|
||||
|
||||
```
|
||||
L'utilisateur sélectionne une zone → capture en screenshot →
|
||||
→ Image uploadée dans FlowDeck → page avec l'image + annotation possible
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Interface utilisateur du serveur
|
||||
|
||||
### 8.1 Extension management dans Settings
|
||||
|
||||
Nouvelle section dans `settings.html` → onglet **"Extensions"** :
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ Connected Extensions │
|
||||
│──────────────────────────────────────────────────────│
|
||||
│ FlowDeck Web Clipper │
|
||||
│ ├── Device: Chrome — Windows │
|
||||
│ ├── Connected: 2026-09-10 14:30 │
|
||||
│ ├── Clips this month: 12 │
|
||||
│ └── [Revoke Access] │
|
||||
│ │
|
||||
│ FlowDeck Web Clipper │
|
||||
│ ├── Device: Firefox — macOS │
|
||||
│ ├── Connected: 2026-09-08 09:15 │
|
||||
│ ├── Clips this month: 5 │
|
||||
│ └── [Revoke Access] │
|
||||
│ │
|
||||
│ [Download Chrome Extension] [Download Firefox Add-on] │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 8.2 Page de téléchargement : `/extensions`
|
||||
|
||||
Nouvelle page publique offrant les liens de téléchargement de l'extension :
|
||||
|
||||
```
|
||||
GET /extensions — Page de téléchargement
|
||||
GET /extensions/chrome — Chrome Web Store link / CRX download
|
||||
GET /extensions/firefox — Firefox Add-on link / XPI download
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Tables de base de données
|
||||
|
||||
### 9.1 Modifications
|
||||
|
||||
```sql
|
||||
-- Extension tracking
|
||||
CREATE TABLE extension_devices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
extension_name TEXT NOT NULL, -- 'chrome', 'firefox', 'edge', 'safari'
|
||||
device_id TEXT NOT NULL, -- UUID unique par installation
|
||||
device_name TEXT, -- "Chrome — Windows 11"
|
||||
token_hash TEXT NOT NULL,
|
||||
scopes TEXT DEFAULT 'read,write',
|
||||
last_used_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
revoked INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(user_id, extension_name, device_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ext_devices_user ON extension_devices(user_id);
|
||||
CREATE INDEX idx_ext_devices_device ON extension_devices(device_id);
|
||||
|
||||
-- Extension activity log
|
||||
CREATE TABLE extension_clips (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
device_id TEXT NOT NULL,
|
||||
clip_type TEXT NOT NULL, -- 'article', 'selection', 'bookmark', 'screenshot'
|
||||
source_url TEXT NOT NULL,
|
||||
target_page_id INTEGER REFERENCES collection_pages(id),
|
||||
target_workspace_id INTEGER,
|
||||
title TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_clips_user ON extension_clips(user_id, created_at);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Sécurité
|
||||
|
||||
| Menace | Contre-mesure |
|
||||
|--------|---------------|
|
||||
| Extension malveillante | Device ID unique + token par device, pas de partage de tokens |
|
||||
| CSRF via extension | Origin checking dans le server-side validation |
|
||||
| Content injection | Sanitisation HTML côté serveur avant création de page |
|
||||
| Données sensibles | Pas de cookies envoyés à l'extension, uniquement tokens OAuth |
|
||||
| Rate limiting | Max 50 clips/heure par device |
|
||||
| Large payload | Max 10 MB par clip (images incluses) |
|
||||
|
||||
### 10.1 Content Security Policy
|
||||
|
||||
```python
|
||||
# Extension CSP étendu
|
||||
# Ajouter dans le CSP de l'app :
|
||||
# connect-src: ws: https://flowdeck.local https://api.flowdeck.local
|
||||
# Content-Security-Policy header pour les pages d'extension
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Tests
|
||||
|
||||
| Test | Description | Outil |
|
||||
|------|-------------|-------|
|
||||
| Clip article | Capturer un article → créer une page FlowDeck | Playwright + HTTP test |
|
||||
| Clip sélection | Sélectionner du texte → créer une page avec le texte | Playwright |
|
||||
| Clip bookmark | Cliquer sur un bookmark → créer une carte bookmark | Unit test |
|
||||
| Auth flow | Extension OAuth → token → clip | Integration test |
|
||||
| Image download | Article avec images → images téléchargées et inline | Unit test |
|
||||
| Multiple devices | 2 extensions connectées → clips séparés | Integration test |
|
||||
| Revoke | Révoquer un device → clips rejetés | Unit test |
|
||||
| Rate limit | 51 clips → 429 Too Many Requests | Unit test |
|
||||
| Content sanitization | HTML malveillant → page propre créée | Security test |
|
||||
| Readability extraction | Page complexe → contenu principal extrait | Unit test |
|
||||
|
||||
---
|
||||
|
||||
## 12. Checklist d'implémentation
|
||||
|
||||
1. **`app/routers/web_clipper.py`** — endpoints clip et auth
|
||||
2. **`app/services/web_clipper.py`** — service de traitement (extraction + création de page)
|
||||
3. **Migrations DB** — `extension_devices`, `extension_clips`, colonne `source` sur `api_tokens`
|
||||
4. **Readability.js intégré** dans le content script
|
||||
5. **Extension côté client** — manifest.json, content.js, background.js, popup.html
|
||||
6. **Settings UI** — section Extensions dans settings.html
|
||||
7. **Page `/extensions`** — téléchargement
|
||||
8. **OAuth pour extension** — device flow ou popup
|
||||
9. **Content sanitization** — nettoyage HTML côté serveur
|
||||
10. **Tests** — tous les scénarios de clipping
|
||||
11. **Packaging** — Chrome Web Store / Firefox Add-on soumission
|
||||
12. **Documentation utilisateur** — guide d'installation et usage
|
||||
|
||||
---
|
||||
|
||||
## 13. Dépendances Python
|
||||
|
||||
```
|
||||
# requirements.txt additions pour v6.0.0 Web Clipper
|
||||
beautifulsoup4>=4.12 # HTML parsing / extraction
|
||||
lxml>=4.9 # XML/HTML parser rapide
|
||||
html2text>=2024.2.26 # HTML → Markdown conversion
|
||||
requests>=2.32 # Download images dans le service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Références
|
||||
|
||||
- [Chrome Extension Manifest V3](https://developer.chrome.com/docs/extensions/mv3/intro/)
|
||||
- [Firefox Add-on Development](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons)
|
||||
- [Readability.js (Mozilla)](https://github.com/mozilla/readability)
|
||||
- [Mercury Parser](https://github.com/postlight/mercury-parser)
|
||||
- [OAuth 2.0 Device Flow](https://datatracker.ietf.org/doc/html/rfc8628)
|
||||
- `app/services/web_clipper.py` — (à créer)
|
||||
- `app/routers/web_clipper.py` — (à créer)
|
||||
- `app/templates/settings.html` — section Extensions à ajouter
|
||||
- `docs/API_GUIDE_V6.md` — référence API v2
|
||||
- `ROADMAP.md` — v6.0.0 Web Clipper item
|
||||
Reference in New Issue
Block a user