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 ## 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` - [ ] Application redémarrée après modification du `.env`
- [ ] Attendu 5-10 minutes après modification dans Google Cloud Console - [ ] 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 ? ## Besoin d'aide ?
Si le problème persiste : Si le problème persiste :
1. Vérifiez les logs de l'application pour des messages d'erreur détaillés 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 2. Pour OAuth 2.0 : Assurez-vous que votre navigateur accepte les cookies tiers
3. Essayez en navigation privée pour éliminer les problèmes de cache 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 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 // Scopes Google Calendar requis
const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']; 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 * Créer un client OAuth2
* @param {string} [customRedirectUri] - URI de redirection personnalisée (optionnel) * @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 * GET /api/calendar/auth-url
* Générer l'URL d'authentification OAuth * 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) => { router.get('/auth-status', async (req, res) => {
try { try {
const tokens = await getOne( const authType = await getAuthType();
'SELECT access_token, expiry_date FROM google_oauth_tokens WHERE user_id = ?',
[req.user.id]
);
const isAuthenticated = !!tokens; if (authType === AUTH_TYPES.SERVICE_ACCOUNT) {
const isExpired = tokens && tokens.expiry_date && tokens.expiry_date < Date.now(); // 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({ res.json({
isAuthenticated, authType: AUTH_TYPES.SERVICE_ACCOUNT,
isExpired, isAuthenticated,
needsReauth: !isAuthenticated || isExpired 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) { } catch (error) {
logger.error('Erreur lors de la vérification du statut OAuth:', error); logger.error('Erreur lors de la vérification du statut OAuth:', error);
res.status(500).json({ error: 'Erreur serveur' }); res.status(500).json({ error: 'Erreur serveur' });
@@ -246,15 +324,23 @@ router.get('/events', async (req, res) => {
/** /**
* POST /api/calendar/sync * POST /api/calendar/sync
* Synchroniser avec Google Calendar via OAuth * Synchroniser avec Google Calendar (OAuth2 ou Service Account)
*/ */
router.post('/sync', async (req, res) => { router.post('/sync', async (req, res) => {
try { try {
// Obtenir un client OAuth valide const authType = await getAuthType();
const oauth2Client = await getValidTokens(req.user.id); let calendarId = 'primary'; // Par défaut : calendrier principal
// Créer un client Google Calendar avec OAuth // Si Service Account, permettre de spécifier l'email du calendrier
const calendar = google.calendar({ version: 'v3', auth: oauth2Client }); 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 // Récupérer les événements des 30 prochains jours
const now = new Date(); const now = new Date();
@@ -262,7 +348,7 @@ router.post('/sync', async (req, res) => {
futureDate.setDate(futureDate.getDate() + 30); futureDate.setDate(futureDate.getDate() + 30);
const response = await calendar.events.list({ const response = await calendar.events.list({
calendarId: 'primary', // Calendrier principal de l'utilisateur calendarId, // Utilise le calendrier configuré
timeMin: now.toISOString(), timeMin: now.toISOString(),
timeMax: futureDate.toISOString(), timeMax: futureDate.toISOString(),
maxResults: 50, maxResults: 50,
+124 -44
View File
@@ -1345,52 +1345,129 @@ const Index = () => {
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<CalendarIcon className="h-5 w-5" /> <CalendarIcon className="h-5 w-5" />
Configuration Google Calendar OAuth 2.0 Configuration Google Calendar
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{/* Choix du type d'authentification */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="google-client-id">Client ID Google</Label> <Label htmlFor="google-auth-type">Méthode d'authentification</Label>
<Input <Select
id="google-client-id" value={settings.google_auth_type || 'oauth2'}
type="text" onValueChange={(value) => setSettings({ ...settings, google_auth_type: value })}
placeholder="123456789-abcdef.apps.googleusercontent.com" >
value={settings.google_client_id || ''} <SelectTrigger id="google-auth-type">
onChange={(e) => setSettings({ ...settings, google_client_id: e.target.value })} <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>
<div className="space-y-2"> {/* Configuration OAuth2 */}
<Label htmlFor="google-client-secret">Client Secret Google</Label> {(!settings.google_auth_type || settings.google_auth_type === 'oauth2') && (
<Input <>
id="google-client-secret" <div className="space-y-2">
type="password" <Label htmlFor="google-client-id">Client ID Google</Label>
placeholder="GOCSPX-..." <Input
value={settings.google_client_secret || ''} id="google-client-id"
onChange={(e) => setSettings({ ...settings, google_client_secret: e.target.value })} type="text"
/> placeholder="123456789-abcdef.apps.googleusercontent.com"
<p className="text-sm text-muted-foreground"> value={settings.google_client_id || ''}
Créez un projet OAuth 2.0 sur{' '} onChange={(e) => setSettings({ ...settings, google_client_id: e.target.value })}
<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline"> />
Google Cloud Console </div>
</a>
{' '}et activez l'API Google Calendar. <div className="space-y-2">
</p> <Label htmlFor="google-client-secret">Client Secret Google</Label>
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg"> <Input
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200 mb-1"> id="google-client-secret"
⚠️ IMPORTANT : URI de redirection autorisée type="password"
</p> placeholder="GOCSPX-..."
<p className="text-xs text-yellow-800 dark:text-yellow-300 mb-2"> value={settings.google_client_secret || ''}
Ajoutez cette URL exacte dans la console Google Cloud (OAuth 2.0 Client IDs → URIs de redirection autorisées) : onChange={(e) => setSettings({ ...settings, google_client_secret: e.target.value })}
</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"> <p className="text-sm text-muted-foreground">
{window.location.origin}/api/calendar/oauth-callback Créez un projet OAuth 2.0 sur{' '}
</code> <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
<p className="text-xs text-yellow-800 dark:text-yellow-300 mt-2"> Google Cloud Console
💡 Si vous utilisez un domaine différent, créez un fichier .env avec APP_URL=https://votre-domaine.com </a>
</p> {' '}et activez l'API Google Calendar.
</div> </p>
</div> <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"> <div className="flex gap-2">
<Button onClick={handleSaveSettings} className="flex items-center gap-2"> <Button onClick={handleSaveSettings} className="flex items-center gap-2">
@@ -1399,14 +1476,17 @@ const Index = () => {
</Button> </Button>
</div> </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"> <div className="border-t pt-4">
<h3 className="font-semibold mb-2">Statut de connexion</h3> <h3 className="font-semibold mb-2">Statut de connexion</h3>
{calendarAuthStatus.isAuthenticated && !calendarAuthStatus.isExpired ? ( {calendarAuthStatus.isAuthenticated && !calendarAuthStatus.isExpired ? (
<div className="flex items-center gap-2 text-green-600"> <div className="flex items-center gap-2 text-green-600">
<div className="h-2 w-2 rounded-full bg-green-600"></div> <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>
) : ( ) : (
<div className="flex items-center gap-2 text-yellow-600"> <div className="flex items-center gap-2 text-yellow-600">
@@ -1417,12 +1497,12 @@ const Index = () => {
</div> </div>
<div className="flex gap-2"> <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"> <Button onClick={handleGoogleConnect} className="flex items-center gap-2">
<CalendarIcon className="h-4 w-4" /> <CalendarIcon className="h-4 w-4" />
Se connecter avec Google Se connecter avec Google
</Button> </Button>
) : ( ) : !calendarAuthStatus.needsReauth && (!settings.google_auth_type || settings.google_auth_type === 'oauth2') ? (
<> <>
<Button <Button
onClick={async () => { onClick={async () => {