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>
12 KiB
✅ 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
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 :
http://localhost:5000/api/calendar/oauth/callback(Backend)http://localhost:5174/calendar/oauth/callback(Frontend)
JavaScript Origins autorisées :
http://localhost:5000http://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
- Allez sur Google Cloud Console
- Sélectionnez le projet
familyplanner-474915 - Menu APIs & Services > OAuth consent screen
- Section Test users > Cliquez sur ADD USERS
- Ajoutez les emails autorisés (le vôtre :
phil.heyraud@gmail.com) - Sauvegardez
Sinon, vous verrez l'erreur : "Access blocked: This app's request is invalid"
🧪 Test de la configuration
1. Redémarrer le backend
cd backend
npm run dev
Vous devriez voir :
Server running on http://localhost:5000
2. Redémarrer le frontend
cd frontend
npm run dev
Vous devriez voir :
Local: http://localhost:5174/
3. Tester la connexion Google Calendar
- Ouvrez l'application dans votre navigateur
- Allez dans les Paramètres du profil
- Section Agendas connectés
- 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 calendrierhttps://www.googleapis.com/auth/calendar.events: Lire/Écrire des événements
Configuré dans : backend/src/routes/calendar.ts ligne 44
🔄 Prochaines étapes
✅ FAIT
- Créer le projet Google Cloud
- Activer l'API Google Calendar
- Créer les credentials OAuth 2.0
- Configurer les URIs de redirection
- Mettre à jour le fichier
.env
⏳ À FAIRE
1. Ajouter utilisateurs de test
- Aller sur OAuth consent screen
- Ajouter
phil.heyraud@gmail.comen 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
// 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
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 :
# 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é :
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
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 :
- OAuth consent screen > PUBLISH APP
- Google examinera votre application (processus de vérification)
- 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
.envdans.gitignore- 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 et mettez à jour :
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 :
- Vérifiez les logs backend : Regardez la console où tourne
npm run dev - Vérifiez la console navigateur : Ouvrez DevTools (F12) > Console
- 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).