Initial commit: Family Planner application
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>
This commit is contained in:
407
docs/archive/OAUTH_CONFIGURATION_COMPLETE.md
Normal file
407
docs/archive/OAUTH_CONFIGURATION_COMPLETE.md
Normal file
@@ -0,0 +1,407 @@
|
||||
# ✅ 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).
|
||||
Reference in New Issue
Block a user