Feature: Ajout du support OAuth2 et Service Account pour Google Calendar

Ajout de deux méthodes d'authentification au choix pour Google Calendar :

1. **OAuth 2.0** (méthode existante améliorée) :
   - ✅ Prompt de sélection de compte Google ('select_account consent')
   - ✅ Gestion dynamique du redirect_uri avec APP_URL
   - ✅ Idéal pour accès au calendrier personnel avec consentement

2. **Service Account** (nouvelle méthode) :
   - ✅ Authentification via clé JSON sans interaction utilisateur
   - ✅ Parfait pour calendriers partagés en entreprise
   - ✅ Synchronisation automatique en arrière-plan
   - ✅ Configuration de l'email du calendrier à synchroniser

## Backend (routes/calendar.routes.js) :
- Ajout des constantes AUTH_TYPES (oauth2, service_account)
- Fonction getAuthType() pour récupérer la méthode configurée
- Fonction getServiceAccountClient() pour Service Account
- Fonction getCalendarClient() unifiée qui choisit la bonne méthode
- Route /auth-status mise à jour pour retourner le type d'auth
- Route /sync adaptée pour supporter les deux méthodes
- Support du calendrier email personnalisé pour Service Account

## Frontend (src/pages/Index.tsx) :
- Menu déroulant pour choisir entre OAuth2 et Service Account
- Interface conditionnelle selon la méthode choisie
- Champ textarea pour la clé JSON du Service Account
- Champ email pour spécifier le calendrier à synchroniser
- Affichage du type d'authentification dans le statut
- Amélioration de l'UI avec descriptions claires

## Documentation :
- OAUTH_SETUP.md : Guide complet mis à jour avec les deux méthodes
  - Section dédiée au choix de la méthode
  - Guide pas à pas pour Service Account
  - Dépannage pour les deux méthodes
- README_GOOGLE_CALENDAR.md : Guide de démarrage rapide (nouveau)
  - Comparaison des deux méthodes
  - Configuration rapide pour chaque méthode
  - Liens vers la documentation complète

Cette implémentation offre une flexibilité maximale pour s'adapter
à différents cas d'usage : personnel (OAuth2) ou professionnel (Service Account).
This commit is contained in:
Claude committed 2025-11-12 13:05:40 +00:00
1 parent 95eba6ce63
commit 3cb6c81489
4 files changed
+415 -65

No files matched your search

+120 -4
View File
@@ -1,6 +1,25 @@
# Configuration OAuth Google Calendar
# Configuration Google Calendar
Ce guide vous aide à résoudre les erreurs d'authentification OAuth Google.
Ce guide vous aide à configurer l'accès à Google Calendar avec deux méthodes disponibles : **OAuth 2.0** et **Service Account**.
## Choisir la bonne méthode
### OAuth 2.0 (Recommandé pour usage personnel)
- ✅ Accès au calendrier personnel de l'utilisateur
- ✅ Sécurisé avec consentement explicite
- ✅ Choix du compte Google à la connexion
- ⚠️ Nécessite une interaction utilisateur (popup de consentement)
### Service Account (Pour applications automatisées)
- ✅ Aucune interaction utilisateur requise
- ✅ Idéal pour calendriers partagés en entreprise
- ✅ Fonctionne en arrière-plan
- ⚠️ Nécessite de partager le calendrier avec le Service Account
- ⚠️ Configuration plus technique
---
# Configuration OAuth 2.0
## Problème : Erreur 400 - redirect_uri_mismatch
@@ -86,10 +105,107 @@ Si vous ne voyez toujours pas le choix du compte :
- [ ] Application redémarrée après modification du `.env`
- [ ] Attendu 5-10 minutes après modification dans Google Cloud Console
---
# Configuration Service Account
Le Service Account permet un accès automatisé sans interaction utilisateur, idéal pour des calendriers partagés.
## Étapes de configuration
### 1. Créer un Service Account
1. Allez sur [Google Cloud Console - Service Accounts](https://console.cloud.google.com/iam-admin/serviceaccounts)
2. Sélectionnez votre projet (ou créez-en un nouveau)
3. Cliquez sur **"+ CREATE SERVICE ACCOUNT"**
4. Donnez un nom au compte (ex: `noteflow-calendar`)
5. Cliquez sur **"CREATE AND CONTINUE"**
6. Sautez les étapes optionnelles et cliquez sur **"DONE"**
### 2. Générer une clé JSON
1. Dans la liste des Service Accounts, cliquez sur celui que vous venez de créer
2. Allez dans l'onglet **"KEYS"**
3. Cliquez sur **"ADD KEY"** > **"Create new key"**
4. Choisissez le format **JSON**
5. Téléchargez le fichier JSON (conservez-le en sécurité !)
### 3. Activer l'API Google Calendar
1. Allez sur [Google Cloud Console - API Library](https://console.cloud.google.com/apis/library)
2. Recherchez **"Google Calendar API"**
3. Cliquez sur **"ENABLE"**
### 4. Partager votre calendrier
1. Ouvrez [Google Calendar](https://calendar.google.com)
2. Trouvez votre calendrier dans la liste à gauche
3. Cliquez sur les **3 points** à côté du nom du calendrier
4. Sélectionnez **"Paramètres et partage"**
5. Faites défiler jusqu'à **"Partager avec des personnes en particulier"**
6. Cliquez sur **"+ Ajouter des personnes"**
7. Collez l'**email du Service Account** (trouvé dans le fichier JSON, champ `client_email`)
8. Donnez les permissions **"Afficher tous les détails des événements"**
9. Cliquez sur **"Envoyer"**
### 5. Configurer NoteFlow
1. Dans NoteFlow, allez dans **Administration** > **Google Calendar**
2. Sélectionnez **"Service Account"** dans le menu déroulant
3. Ouvrez le fichier JSON téléchargé et **copiez tout son contenu**
4. Collez le contenu dans le champ **"Clé Service Account (JSON)"**
5. Entrez votre email (celui du calendrier partagé) dans **"Email du calendrier à synchroniser"**
6. Cliquez sur **"Enregistrer"**
7. Cliquez sur **"Synchroniser"** pour tester
## Dépannage Service Account
### Erreur : "Clé Service Account invalide"
- Vérifiez que vous avez copié l'intégralité du fichier JSON
- Assurez-vous qu'il n'y a pas d'espaces ou de caractères en trop
### Erreur : "Calendar not found" ou "Permission denied"
- Vérifiez que vous avez bien partagé le calendrier avec l'email du Service Account
- Vérifiez que l'email du calendrier est correct
- Attendez quelques minutes après avoir partagé le calendrier
### Les événements ne se synchronisent pas
- Vérifiez que l'API Google Calendar est activée dans votre projet
- Vérifiez les logs de l'application pour plus de détails
- Assurez-vous que le Service Account a les bonnes permissions
---
## Besoin d'aide ?
Si le problème persiste :
1. Vérifiez les logs de l'application pour des messages d'erreur détaillés
2. Assurez-vous que votre navigateur accepte les cookies tiers
3. Essayez en navigation privée pour éliminer les problèmes de cache
2. Pour OAuth 2.0 : Assurez-vous que votre navigateur accepte les cookies tiers
3. Pour OAuth 2.0 : Essayez en navigation privée pour éliminer les problèmes de cache
4. Vérifiez que l'API Google Calendar est bien activée dans votre projet Google Cloud
5. Pour Service Account : Vérifiez que le calendrier est bien partagé avec le bon email
+68
View File
@@ -0,0 +1,68 @@
# Google Calendar - Deux méthodes d'authentification
NoteFlow supporte maintenant **deux méthodes** pour se connecter à Google Calendar :
## 🔐 OAuth 2.0 (Recommandé pour usage personnel)
**Avantages** :
- Accès sécurisé à votre calendrier personnel
- Choix du compte Google lors de la connexion
- Consentement explicite de l'utilisateur
**Utilisation** : Administration > Google Calendar > Méthode : OAuth 2.0
[📖 Guide complet OAuth 2.0](./OAUTH_SETUP.md#configuration-oauth-20)
---
## 🤖 Service Account (Pour automatisation)
**Avantages** :
- Aucune interaction utilisateur requise
- Parfait pour calendriers partagés en entreprise
- Fonctionne en arrière-plan automatiquement
**Utilisation** : Administration > Google Calendar > Méthode : Service Account
[📖 Guide complet Service Account](./OAUTH_SETUP.md#configuration-service-account)
---
## Problèmes courants résolus
### ✅ Erreur 400 : redirect_uri_mismatch
- Variable `APP_URL` ajoutée dans `.env.example`
- Guide de configuration détaillé dans OAUTH_SETUP.md
### ✅ Pas de choix de compte Google
- Le prompt `select_account` force maintenant l'affichage du sélecteur
### ✅ Besoin d'accès automatisé sans popup
- Utilisez Service Account pour une authentification sans interaction
---
## Configuration rapide
### Pour OAuth 2.0 :
1. Créez `.env` avec `APP_URL=http://localhost:2222`
2. Configurez OAuth 2.0 dans Google Cloud Console
3. Dans NoteFlow : Administration > Google Calendar > OAuth 2.0
4. Ajoutez Client ID et Client Secret
5. Cliquez sur "Se connecter avec Google"
### Pour Service Account :
1. Créez un Service Account sur Google Cloud Console
2. Téléchargez la clé JSON
3. Partagez votre calendrier avec l'email du Service Account
4. Dans NoteFlow : Administration > Google Calendar > Service Account
5. Collez le JSON et l'email du calendrier
6. Cliquez sur "Synchroniser"
---
## Documentation complète
📚 Consultez [OAUTH_SETUP.md](./OAUTH_SETUP.md) pour :
- Guide pas à pas complet
- Résolution des erreurs
- Checklist de vérification
- Conseils de dépannage
+103 -17
View File
@@ -12,6 +12,22 @@ router.use(authenticateToken);
// Scopes Google Calendar requis
const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'];
/**
* Types d'authentification supportés
*/
const AUTH_TYPES = {
OAUTH2: 'oauth2',
SERVICE_ACCOUNT: 'service_account'
};
/**
* Récupérer le type d'authentification configuré
*/
async function getAuthType() {
const authType = await getOne("SELECT value FROM settings WHERE key = 'google_auth_type'");
return authType?.value || AUTH_TYPES.OAUTH2; // Par défaut OAuth2
}
/**
* Créer un client OAuth2
* @param {string} [customRedirectUri] - URI de redirection personnalisée (optionnel)
@@ -35,6 +51,51 @@ async function getOAuth2Client(customRedirectUri = null) {
);
}
/**
* Créer un client avec Service Account
*/
async function getServiceAccountClient() {
const serviceAccountKey = await getOne("SELECT value FROM settings WHERE key = 'google_service_account_key'");
if (!serviceAccountKey || !serviceAccountKey.value) {
throw new Error('Clé Service Account non configurée');
}
try {
const credentials = JSON.parse(serviceAccountKey.value);
const auth = new google.auth.GoogleAuth({
credentials,
scopes: SCOPES
});
return await auth.getClient();
} catch (error) {
logger.error('Erreur lors de la création du client Service Account:', error);
throw new Error('Clé Service Account invalide');
}
}
/**
* Créer un client Google Calendar selon la méthode d'authentification
* @param {number} userId - ID de l'utilisateur (pour OAuth2)
*/
async function getCalendarClient(userId = null) {
const authType = await getAuthType();
if (authType === AUTH_TYPES.SERVICE_ACCOUNT) {
const auth = await getServiceAccountClient();
return google.calendar({ version: 'v3', auth });
} else {
// OAuth2
if (!userId) {
throw new Error('User ID requis pour OAuth2');
}
const oauth2Client = await getValidTokens(userId);
return google.calendar({ version: 'v3', auth: oauth2Client });
}
}
/**
* GET /api/calendar/auth-url
* Générer l'URL d'authentification OAuth
@@ -153,19 +214,36 @@ router.get('/oauth-callback', async (req, res) => {
*/
router.get('/auth-status', async (req, res) => {
try {
const tokens = await getOne(
'SELECT access_token, expiry_date FROM google_oauth_tokens WHERE user_id = ?',
[req.user.id]
);
const authType = await getAuthType();
const isAuthenticated = !!tokens;
const isExpired = tokens && tokens.expiry_date && tokens.expiry_date < Date.now();
if (authType === AUTH_TYPES.SERVICE_ACCOUNT) {
// Vérifier si la clé Service Account est configurée
const serviceAccountKey = await getOne("SELECT value FROM settings WHERE key = 'google_service_account_key'");
const isAuthenticated = !!serviceAccountKey?.value;
res.json({
isAuthenticated,
isExpired,
needsReauth: !isAuthenticated || isExpired
});
res.json({
authType: AUTH_TYPES.SERVICE_ACCOUNT,
isAuthenticated,
isExpired: false,
needsReauth: !isAuthenticated
});
} else {
// OAuth2
const tokens = await getOne(
'SELECT access_token, expiry_date FROM google_oauth_tokens WHERE user_id = ?',
[req.user.id]
);
const isAuthenticated = !!tokens;
const isExpired = tokens && tokens.expiry_date && tokens.expiry_date < Date.now();
res.json({
authType: AUTH_TYPES.OAUTH2,
isAuthenticated,
isExpired,
needsReauth: !isAuthenticated || isExpired
});
}
} catch (error) {
logger.error('Erreur lors de la vérification du statut OAuth:', error);
res.status(500).json({ error: 'Erreur serveur' });
@@ -246,15 +324,23 @@ router.get('/events', async (req, res) => {
/**
* POST /api/calendar/sync
* Synchroniser avec Google Calendar via OAuth
* Synchroniser avec Google Calendar (OAuth2 ou Service Account)
*/
router.post('/sync', async (req, res) => {
try {
// Obtenir un client OAuth valide
const oauth2Client = await getValidTokens(req.user.id);
const authType = await getAuthType();
let calendarId = 'primary'; // Par défaut : calendrier principal
// Créer un client Google Calendar avec OAuth
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
// Si Service Account, permettre de spécifier l'email du calendrier
if (authType === AUTH_TYPES.SERVICE_ACCOUNT) {
const calendarEmail = await getOne("SELECT value FROM settings WHERE key = 'google_calendar_email'");
if (calendarEmail?.value) {
calendarId = calendarEmail.value;
}
}
// Créer un client Google Calendar selon la méthode d'authentification
const calendar = await getCalendarClient(req.user.id);
// Récupérer les événements des 30 prochains jours
const now = new Date();
@@ -262,7 +348,7 @@ router.post('/sync', async (req, res) => {
futureDate.setDate(futureDate.getDate() + 30);
const response = await calendar.events.list({
calendarId: 'primary', // Calendrier principal de l'utilisateur
calendarId, // Utilise le calendrier configuré
timeMin: now.toISOString(),
timeMax: futureDate.toISOString(),
maxResults: 50,
+124 -44
View File
@@ -1345,52 +1345,129 @@ const Index = () => {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CalendarIcon className="h-5 w-5" />
Configuration Google Calendar OAuth 2.0
Configuration Google Calendar
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Choix du type d'authentification */}
<div className="space-y-2">
<Label htmlFor="google-client-id">Client ID Google</Label>
<Input
id="google-client-id"
type="text"
placeholder="123456789-abcdef.apps.googleusercontent.com"
value={settings.google_client_id || ''}
onChange={(e) => setSettings({ ...settings, google_client_id: e.target.value })}
/>
<Label htmlFor="google-auth-type">Méthode d'authentification</Label>
<Select
value={settings.google_auth_type || 'oauth2'}
onValueChange={(value) => setSettings({ ...settings, google_auth_type: value })}
>
<SelectTrigger id="google-auth-type">
<SelectValue placeholder="Sélectionnez une méthode" />
</SelectTrigger>
<SelectContent>
<SelectItem value="oauth2">
<div className="flex flex-col">
<span className="font-medium">OAuth 2.0</span>
<span className="text-xs text-muted-foreground">
Calendrier personnel avec consentement utilisateur
</span>
</div>
</SelectItem>
<SelectItem value="service_account">
<div className="flex flex-col">
<span className="font-medium">Service Account</span>
<span className="text-xs text-muted-foreground">
Clé JSON pour calendriers partagés (sans interaction)
</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
<strong>OAuth 2.0</strong> : Pour accéder au calendrier personnel avec autorisation de l'utilisateur.<br/>
<strong>Service Account</strong> : Pour accéder à des calendriers partagés sans interaction utilisateur.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="google-client-secret">Client Secret Google</Label>
<Input
id="google-client-secret"
type="password"
placeholder="GOCSPX-..."
value={settings.google_client_secret || ''}
onChange={(e) => setSettings({ ...settings, google_client_secret: e.target.value })}
/>
<p className="text-sm text-muted-foreground">
Créez un projet OAuth 2.0 sur{' '}
<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
Google Cloud Console
</a>
{' '}et activez l'API Google Calendar.
</p>
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200 mb-1">
⚠️ IMPORTANT : URI de redirection autorisée
</p>
<p className="text-xs text-yellow-800 dark:text-yellow-300 mb-2">
Ajoutez cette URL exacte dans la console Google Cloud (OAuth 2.0 Client IDs → URIs de redirection autorisées) :
</p>
<code className="block p-2 bg-white dark:bg-gray-800 border border-yellow-300 dark:border-yellow-700 rounded text-xs font-mono break-all">
{window.location.origin}/api/calendar/oauth-callback
</code>
<p className="text-xs text-yellow-800 dark:text-yellow-300 mt-2">
💡 Si vous utilisez un domaine différent, créez un fichier .env avec APP_URL=https://votre-domaine.com
</p>
</div>
</div>
{/* Configuration OAuth2 */}
{(!settings.google_auth_type || settings.google_auth_type === 'oauth2') && (
<>
<div className="space-y-2">
<Label htmlFor="google-client-id">Client ID Google</Label>
<Input
id="google-client-id"
type="text"
placeholder="123456789-abcdef.apps.googleusercontent.com"
value={settings.google_client_id || ''}
onChange={(e) => setSettings({ ...settings, google_client_id: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="google-client-secret">Client Secret Google</Label>
<Input
id="google-client-secret"
type="password"
placeholder="GOCSPX-..."
value={settings.google_client_secret || ''}
onChange={(e) => setSettings({ ...settings, google_client_secret: e.target.value })}
/>
<p className="text-sm text-muted-foreground">
Créez un projet OAuth 2.0 sur{' '}
<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
Google Cloud Console
</a>
{' '}et activez l'API Google Calendar.
</p>
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200 mb-1">
⚠️ IMPORTANT : URI de redirection autorisée
</p>
<p className="text-xs text-yellow-800 dark:text-yellow-300 mb-2">
Ajoutez cette URL exacte dans la console Google Cloud (OAuth 2.0 Client IDs → URIs de redirection autorisées) :
</p>
<code className="block p-2 bg-white dark:bg-gray-800 border border-yellow-300 dark:border-yellow-700 rounded text-xs font-mono break-all">
{window.location.origin}/api/calendar/oauth-callback
</code>
<p className="text-xs text-yellow-800 dark:text-yellow-300 mt-2">
💡 Si vous utilisez un domaine différent, créez un fichier .env avec APP_URL=https://votre-domaine.com
</p>
</div>
</div>
</>
)}
{/* Configuration Service Account */}
{settings.google_auth_type === 'service_account' && (
<>
<div className="space-y-2">
<Label htmlFor="google-service-account-key">Clé Service Account (JSON)</Label>
<textarea
id="google-service-account-key"
className="w-full min-h-[200px] p-3 text-xs font-mono border rounded-md"
placeholder='Collez ici le contenu du fichier JSON de votre Service Account&#10;{&#10; "type": "service_account",&#10; "project_id": "...",&#10; "private_key_id": "...",&#10; ...&#10;}'
value={settings.google_service_account_key || ''}
onChange={(e) => setSettings({ ...settings, google_service_account_key: e.target.value })}
/>
<p className="text-sm text-muted-foreground">
Créez un Service Account sur{' '}
<a href="https://console.cloud.google.com/iam-admin/serviceaccounts" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
Google Cloud Console
</a>
{' '}et téléchargez la clé JSON.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="google-calendar-email">Email du calendrier à synchroniser</Label>
<Input
id="google-calendar-email"
type="email"
placeholder="votre-email@gmail.com"
value={settings.google_calendar_email || ''}
onChange={(e) => setSettings({ ...settings, google_calendar_email: e.target.value })}
/>
<p className="text-xs text-muted-foreground">
⚠️ Important : Vous devez partager votre calendrier avec l'email du Service Account (visible dans le JSON)
</p>
</div>
</>
)}
<div className="flex gap-2">
<Button onClick={handleSaveSettings} className="flex items-center gap-2">
@@ -1399,14 +1476,17 @@ const Index = () => {
</Button>
</div>
{settings.google_client_id && settings.google_client_secret && (
{(
((!settings.google_auth_type || settings.google_auth_type === 'oauth2') && settings.google_client_id && settings.google_client_secret) ||
(settings.google_auth_type === 'service_account' && settings.google_service_account_key)
) && (
<>
<div className="border-t pt-4">
<h3 className="font-semibold mb-2">Statut de connexion</h3>
{calendarAuthStatus.isAuthenticated && !calendarAuthStatus.isExpired ? (
<div className="flex items-center gap-2 text-green-600">
<div className="h-2 w-2 rounded-full bg-green-600"></div>
<span>Connecté à Google Calendar</span>
<span>Connecté à Google Calendar ({calendarAuthStatus.authType === 'service_account' ? 'Service Account' : 'OAuth 2.0'})</span>
</div>
) : (
<div className="flex items-center gap-2 text-yellow-600">
@@ -1417,12 +1497,12 @@ const Index = () => {
</div>
<div className="flex gap-2">
{calendarAuthStatus.needsReauth ? (
{calendarAuthStatus.needsReauth && (!settings.google_auth_type || settings.google_auth_type === 'oauth2') ? (
<Button onClick={handleGoogleConnect} className="flex items-center gap-2">
<CalendarIcon className="h-4 w-4" />
Se connecter avec Google
</Button>
) : (
) : !calendarAuthStatus.needsReauth && (!settings.google_auth_type || settings.google_auth_type === 'oauth2') ? (
<>
<Button
onClick={async () => {