Complete family planning application with: - React frontend with TypeScript - Node.js/Express backend with TypeScript - Python ingestion service for document processing - Planning ingestion service with LLM integration - Shared UI components and type definitions - OAuth integration for calendar synchronization - Comprehensive documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
408 lines
12 KiB
Markdown
408 lines
12 KiB
Markdown
# ✅ Configuration OAuth Google Calendar - TERMINÉE
|
|
|
|
## 📋 Récapitulatif de la configuration
|
|
|
|
### ✅ Google OAuth Client créé avec succès
|
|
|
|
**Projet Google Cloud** : `familyplanner-474915`
|
|
**Date de création** : 12 octobre 2025 à 17:51:35 GMT+2
|
|
**État** : ✅ Activé
|
|
|
|
---
|
|
|
|
## 🔑 Credentials configurées
|
|
|
|
### Backend `.env` mis à jour
|
|
|
|
```env
|
|
GOOGLE_CLIENT_ID=645971045469-1f9kliea9lqhutjeicim377fui2kdhc8.apps.googleusercontent.com
|
|
GOOGLE_CLIENT_SECRET=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
|
|
GOOGLE_REDIRECT_URI=http://localhost:5000/api/calendar/oauth/callback
|
|
```
|
|
|
|
✅ **Sécurité** : Le fichier `.env` est bien dans `.gitignore` (ligne 4)
|
|
|
|
---
|
|
|
|
## 🌐 URIs de redirection autorisées
|
|
|
|
Les URIs suivantes sont configurées dans Google Cloud Console :
|
|
|
|
1. `http://localhost:5000/api/calendar/oauth/callback` (Backend)
|
|
2. `http://localhost:5174/calendar/oauth/callback` (Frontend)
|
|
|
|
**JavaScript Origins autorisées** :
|
|
1. `http://localhost:5000`
|
|
2. `http://localhost:5174`
|
|
|
|
---
|
|
|
|
## ⚠️ Écran de consentement OAuth
|
|
|
|
**Important** : L'accès OAuth est actuellement réservé aux **utilisateurs de test** listés sur votre écran de consentement OAuth.
|
|
|
|
### Pour ajouter des utilisateurs de test
|
|
|
|
1. Allez sur [Google Cloud Console](https://console.cloud.google.com/)
|
|
2. Sélectionnez le projet `familyplanner-474915`
|
|
3. Menu **APIs & Services** > **OAuth consent screen**
|
|
4. Section **Test users** > Cliquez sur **ADD USERS**
|
|
5. Ajoutez les emails autorisés (le vôtre : `phil.heyraud@gmail.com`)
|
|
6. Sauvegardez
|
|
|
|
**Sinon**, vous verrez l'erreur : "Access blocked: This app's request is invalid"
|
|
|
|
---
|
|
|
|
## 🧪 Test de la configuration
|
|
|
|
### 1. Redémarrer le backend
|
|
|
|
```bash
|
|
cd backend
|
|
npm run dev
|
|
```
|
|
|
|
Vous devriez voir :
|
|
```
|
|
Server running on http://localhost:5000
|
|
```
|
|
|
|
### 2. Redémarrer le frontend
|
|
|
|
```bash
|
|
cd frontend
|
|
npm run dev
|
|
```
|
|
|
|
Vous devriez voir :
|
|
```
|
|
Local: http://localhost:5174/
|
|
```
|
|
|
|
### 3. Tester la connexion Google Calendar
|
|
|
|
1. Ouvrez l'application dans votre navigateur
|
|
2. Allez dans les **Paramètres du profil**
|
|
3. Section **Agendas connectés**
|
|
4. Cliquez sur **"Continuer avec Google"**
|
|
|
|
**Résultat attendu** :
|
|
- ✅ Redirection vers `accounts.google.com`
|
|
- ✅ Page de consentement Google affichée
|
|
- ✅ Demande d'autorisation pour accéder au calendrier
|
|
- ✅ Après acceptation, retour vers l'application
|
|
|
|
**Si erreur** :
|
|
- ❌ "Access blocked" → Ajoutez votre email en utilisateur de test
|
|
- ❌ "redirect_uri_mismatch" → Vérifiez les URIs dans la console
|
|
- ❌ "invalid_client" → Vérifiez le Client ID dans `.env`
|
|
|
|
---
|
|
|
|
## 📊 Scopes autorisés
|
|
|
|
Les scopes suivants sont demandés lors de l'authentification :
|
|
|
|
- `https://www.googleapis.com/auth/calendar.readonly` : Lire les événements du calendrier
|
|
- `https://www.googleapis.com/auth/calendar.events` : Lire/Écrire des événements
|
|
|
|
**Configuré dans** : `backend/src/routes/calendar.ts` ligne 44
|
|
|
|
---
|
|
|
|
## 🔄 Prochaines étapes
|
|
|
|
### ✅ FAIT
|
|
- [x] Créer le projet Google Cloud
|
|
- [x] Activer l'API Google Calendar
|
|
- [x] Créer les credentials OAuth 2.0
|
|
- [x] Configurer les URIs de redirection
|
|
- [x] Mettre à jour le fichier `.env`
|
|
|
|
### ⏳ À FAIRE
|
|
|
|
#### 1. Ajouter utilisateurs de test
|
|
- [ ] Aller sur OAuth consent screen
|
|
- [ ] Ajouter `phil.heyraud@gmail.com` en utilisateur de test
|
|
- [ ] Ajouter d'autres emails si besoin
|
|
|
|
#### 2. Implémenter l'échange de code OAuth
|
|
Actuellement, le code d'autorisation n'est pas échangé contre un access token.
|
|
|
|
**Fichier à modifier** : `backend/src/routes/calendar.ts`
|
|
|
|
```typescript
|
|
// Ligne ~90 : endpoint /oauth/complete
|
|
calendarRouter.post("/oauth/complete", async (req: Request, res: Response) => {
|
|
try {
|
|
const { provider, state, code, error, profileId } = oauthCompleteBody.parse(req.body ?? {});
|
|
|
|
if (error) return res.json({ success: false, error });
|
|
if (!code) return res.json({ success: false, error: "No authorization code" });
|
|
|
|
const pending = pendingStates.get(state);
|
|
if (!pending || pending.expiresAt < Date.now()) {
|
|
return res.status(400).json({ success: false, error: "State expired or invalid" });
|
|
}
|
|
pendingStates.delete(state);
|
|
|
|
// ⚠️ NOUVEAU : Échanger le code contre un access token
|
|
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
code: code,
|
|
client_id: process.env.GOOGLE_CLIENT_ID,
|
|
client_secret: process.env.GOOGLE_CLIENT_SECRET,
|
|
redirect_uri: process.env.GOOGLE_REDIRECT_URI,
|
|
grant_type: "authorization_code"
|
|
})
|
|
});
|
|
|
|
if (!tokenResponse.ok) {
|
|
const errorData = await tokenResponse.json();
|
|
console.error("Token exchange failed:", errorData);
|
|
return res.json({ success: false, error: "Token exchange failed" });
|
|
}
|
|
|
|
const tokens = await tokenResponse.json();
|
|
// tokens.access_token, tokens.refresh_token, tokens.expires_in
|
|
|
|
// ⚠️ IMPORTANT : Chiffrer et stocker les tokens de manière sécurisée
|
|
const encryptedToken = await encryptToken(tokens.access_token);
|
|
const encryptedRefreshToken = tokens.refresh_token
|
|
? await encryptToken(tokens.refresh_token)
|
|
: null;
|
|
|
|
const pid = profileId ?? pending.profileId;
|
|
const list = connectionsByProfile.get(pid) ?? [];
|
|
const conn: ConnectedCalendar = {
|
|
id: pending.connectionId,
|
|
provider,
|
|
email: await getUserEmail(tokens.access_token), // Récupérer l'email réel
|
|
label: `Google Calendar (${new Date().toLocaleDateString()})`,
|
|
status: "connected",
|
|
createdAt: new Date().toISOString(),
|
|
lastSyncedAt: new Date().toISOString(),
|
|
scopes: tokens.scope.split(" "),
|
|
shareWithFamily: false
|
|
};
|
|
|
|
// Stocker les tokens chiffrés dans la base de données (pas en mémoire)
|
|
await saveEncryptedTokens(conn.id, {
|
|
accessToken: encryptedToken,
|
|
refreshToken: encryptedRefreshToken,
|
|
expiresAt: new Date(Date.now() + tokens.expires_in * 1000)
|
|
});
|
|
|
|
connectionsByProfile.set(pid, [...list, conn]);
|
|
res.json({
|
|
success: true,
|
|
email: conn.email,
|
|
label: conn.label,
|
|
connectionId: conn.id,
|
|
profileId: pid
|
|
});
|
|
} catch (e) {
|
|
console.error("OAuth complete error:", e);
|
|
res.status(400).json({ success: false, error: "OAuth completion failed" });
|
|
}
|
|
});
|
|
|
|
// Helper pour récupérer l'email de l'utilisateur
|
|
async function getUserEmail(accessToken: string): Promise<string> {
|
|
const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
|
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
});
|
|
const data = await response.json();
|
|
return data.email;
|
|
}
|
|
```
|
|
|
|
#### 3. Implémenter le chiffrement des tokens
|
|
|
|
**Créer** : `backend/src/utils/encryption.ts`
|
|
|
|
```typescript
|
|
import crypto from "crypto";
|
|
|
|
const ALGORITHM = "aes-256-gcm";
|
|
const ENCRYPTION_KEY = process.env.TOKEN_ENCRYPTION_KEY; // 32 bytes hex
|
|
|
|
if (!ENCRYPTION_KEY) {
|
|
throw new Error("TOKEN_ENCRYPTION_KEY is required in .env");
|
|
}
|
|
|
|
export function encryptToken(token: string): {
|
|
encrypted: string;
|
|
iv: string;
|
|
authTag: string;
|
|
} {
|
|
const iv = crypto.randomBytes(16);
|
|
const cipher = crypto.createCipheriv(
|
|
ALGORITHM,
|
|
Buffer.from(ENCRYPTION_KEY, "hex"),
|
|
iv
|
|
);
|
|
|
|
let encrypted = cipher.update(token, "utf8", "hex");
|
|
encrypted += cipher.final("hex");
|
|
|
|
return {
|
|
encrypted,
|
|
iv: iv.toString("hex"),
|
|
authTag: cipher.getAuthTag().toString("hex")
|
|
};
|
|
}
|
|
|
|
export function decryptToken(
|
|
encrypted: string,
|
|
iv: string,
|
|
authTag: string
|
|
): string {
|
|
const decipher = crypto.createDecipheriv(
|
|
ALGORITHM,
|
|
Buffer.from(ENCRYPTION_KEY, "hex"),
|
|
Buffer.from(iv, "hex")
|
|
);
|
|
|
|
decipher.setAuthTag(Buffer.from(authTag, "hex"));
|
|
|
|
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
decrypted += decipher.final("utf8");
|
|
|
|
return decrypted;
|
|
}
|
|
```
|
|
|
|
**Ajouter dans `.env`** :
|
|
```env
|
|
# Token Encryption (générez une clé aléatoire de 32 bytes)
|
|
TOKEN_ENCRYPTION_KEY=votre_clé_32_bytes_hex_ici
|
|
```
|
|
|
|
**Générer une clé** :
|
|
```bash
|
|
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
```
|
|
|
|
#### 4. Implémenter la récupération des événements
|
|
|
|
**Créer** : `backend/src/services/google-calendar.service.ts`
|
|
|
|
```typescript
|
|
export class GoogleCalendarService {
|
|
async getEvents(
|
|
accessToken: string,
|
|
startDate: Date,
|
|
endDate: Date
|
|
): Promise<CalendarEvent[]> {
|
|
const params = new URLSearchParams({
|
|
timeMin: startDate.toISOString(),
|
|
timeMax: endDate.toISOString(),
|
|
maxResults: "250",
|
|
singleEvents: "true",
|
|
orderBy: "startTime"
|
|
});
|
|
|
|
const response = await fetch(
|
|
`https://www.googleapis.com/calendar/v3/calendars/primary/events?${params}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
"Content-Type": "application/json"
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
// Token expiré, besoin de refresh
|
|
throw new Error("TOKEN_EXPIRED");
|
|
}
|
|
throw new Error(`Google Calendar API error: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return this.normalizeEvents(data.items);
|
|
}
|
|
|
|
private normalizeEvents(items: any[]): CalendarEvent[] {
|
|
return items.map(event => ({
|
|
id: event.id,
|
|
title: event.summary || "(Sans titre)",
|
|
description: event.description,
|
|
startDate: event.start.dateTime || event.start.date,
|
|
endDate: event.end.dateTime || event.end.date,
|
|
isAllDay: !event.start.dateTime,
|
|
location: event.location,
|
|
attendees: event.attendees?.map((a: any) => a.email),
|
|
source: "google"
|
|
}));
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 5. Publier l'application (plus tard)
|
|
|
|
Actuellement, votre app est en mode "Testing" avec utilisateurs limités.
|
|
|
|
**Pour rendre publique** :
|
|
1. OAuth consent screen > **PUBLISH APP**
|
|
2. Google examinera votre application (processus de vérification)
|
|
3. Une fois approuvé, tout le monde pourra se connecter
|
|
|
|
**Note** : Pas nécessaire pour l'instant si vous testez uniquement avec votre compte.
|
|
|
|
---
|
|
|
|
## 🔐 Sécurité - Checklist
|
|
|
|
- [x] `.env` dans `.gitignore`
|
|
- [x] Client secret stocké uniquement dans `.env`
|
|
- [ ] Générer clé de chiffrement `TOKEN_ENCRYPTION_KEY`
|
|
- [ ] Implémenter chiffrement des tokens
|
|
- [ ] Stocker tokens chiffrés dans une base de données (pas en mémoire)
|
|
- [ ] Révoquer l'ancienne clé OpenAI exposée
|
|
- [ ] Ajouter middleware d'authentification sur les routes
|
|
|
|
---
|
|
|
|
## 📱 Configuration Outlook (optionnel)
|
|
|
|
Si vous voulez aussi connecter Outlook/Microsoft 365, suivez le même processus sur [Azure Portal](https://portal.azure.com/) et mettez à jour :
|
|
|
|
```env
|
|
OUTLOOK_CLIENT_ID=votre_application_id_ici
|
|
OUTLOOK_CLIENT_SECRET=votre_secret_ici
|
|
OUTLOOK_REDIRECT_URI=http://localhost:5000/api/calendar/oauth/callback
|
|
```
|
|
|
|
---
|
|
|
|
## 📞 Support
|
|
|
|
**En cas de problème** :
|
|
|
|
1. **Vérifiez les logs backend** : Regardez la console où tourne `npm run dev`
|
|
2. **Vérifiez la console navigateur** : Ouvrez DevTools (F12) > Console
|
|
3. **Testez l'URL OAuth manuellement** :
|
|
```
|
|
https://accounts.google.com/o/oauth2/v2/auth?client_id=645971045469-1f9kliea9lqhutjeicim377fui2kdhc8.apps.googleusercontent.com&redirect_uri=http://localhost:5000/api/calendar/oauth/callback&response_type=code&scope=https://www.googleapis.com/auth/calendar.readonly&state=test123&access_type=offline&prompt=consent
|
|
```
|
|
|
|
**Erreurs communes** :
|
|
- "invalid_client" → Client ID incorrect dans `.env`
|
|
- "redirect_uri_mismatch" → URI pas exactement identique dans console Google
|
|
- "access_denied" → Utilisateur a refusé les permissions
|
|
- "unauthorized_client" → App pas publiée et utilisateur pas en mode test
|
|
|
|
---
|
|
|
|
## ✅ Configuration terminée avec succès !
|
|
|
|
Vous pouvez maintenant tester la connexion Google Calendar depuis votre application. 🎉
|
|
|
|
**Prochaine étape** : Implémentez l'échange du code OAuth et la récupération des événements (voir sections ci-dessus).
|