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>
847 lines
22 KiB
Markdown
847 lines
22 KiB
Markdown
# Analyse complète du code Calendar - Sécurité, Fiabilité, Performance, Qualité
|
|
|
|
## 📋 Vue d'ensemble
|
|
|
|
**Fichier analysé** : `backend/src/routes/calendar.ts`
|
|
**Lignes de code** : ~180
|
|
**Dépendances** : Express, Zod, Node Crypto
|
|
**Fonctionnalités** : OAuth Google/Outlook, gestion connexions calendrier
|
|
|
|
---
|
|
|
|
## 🔒 Sécurité : 3/10 ⚠️ CRITIQUE
|
|
|
|
### ❌ Vulnérabilités critiques
|
|
|
|
#### 1. **Stockage des tokens en mémoire (lignes 21-22)**
|
|
```typescript
|
|
const connectionsByProfile = new Map<string, ConnectedCalendar[]>();
|
|
const pendingStates = new Map<string, { profileId: string; provider: CalendarProvider; connectionId: string }>();
|
|
```
|
|
|
|
**Risques** :
|
|
- Perte totale des connexions au redémarrage du serveur
|
|
- Pas de persistance
|
|
- Pas de chiffrement
|
|
- Exposition en cas de dump mémoire
|
|
|
|
**Impact** : 🔴 CRITIQUE
|
|
**Recommandation** :
|
|
```typescript
|
|
// Utiliser une base de données avec chiffrement
|
|
import { Database } from "./database";
|
|
import { TokenEncryption } from "./security/encryption";
|
|
|
|
class SecureTokenStorage {
|
|
async saveToken(userId: string, token: string): Promise<void> {
|
|
const { encrypted, iv, authTag } = TokenEncryption.encrypt(token);
|
|
await Database.tokens.insert({
|
|
userId,
|
|
encryptedToken: encrypted,
|
|
iv,
|
|
authTag,
|
|
createdAt: new Date()
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2. **Client secrets dans le code (lignes 34-48)**
|
|
```typescript
|
|
const OAUTH_CONFIG = {
|
|
google: {
|
|
clientId: process.env.GOOGLE_CLIENT_ID || "YOUR_GOOGLE_CLIENT_ID", // ❌
|
|
// ...
|
|
}
|
|
};
|
|
```
|
|
|
|
**Risques** :
|
|
- Valeurs par défaut dangereuses
|
|
- Pas de validation des variables d'environnement
|
|
- Potentiel leak si `.env` n'est pas dans `.gitignore`
|
|
|
|
**Impact** : 🔴 CRITIQUE
|
|
**Recommandation** :
|
|
```typescript
|
|
function getRequiredEnv(key: string): string {
|
|
const value = process.env[key];
|
|
if (!value || value.startsWith("YOUR_") || value.includes("_here")) {
|
|
throw new Error(`Missing required environment variable: ${key}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
const OAUTH_CONFIG = {
|
|
google: {
|
|
clientId: getRequiredEnv("GOOGLE_CLIENT_ID"),
|
|
clientSecret: getRequiredEnv("GOOGLE_CLIENT_SECRET"),
|
|
redirectUri: getRequiredEnv("GOOGLE_REDIRECT_URI")
|
|
}
|
|
};
|
|
```
|
|
|
|
#### 3. **Pas de validation du state OAuth (ligne 99)**
|
|
```typescript
|
|
const pending = pendingStates.get(state);
|
|
if (!pending) return res.json({ success: false, error: "Invalid state" });
|
|
```
|
|
|
|
**Risques** :
|
|
- Attaque CSRF possible
|
|
- Pas d'expiration du state
|
|
- Pas de vérification de l'origine
|
|
|
|
**Impact** : 🟠 MOYEN
|
|
**Recommandation** :
|
|
```typescript
|
|
type PendingState = {
|
|
profileId: string;
|
|
provider: CalendarProvider;
|
|
connectionId: string;
|
|
createdAt: number;
|
|
expiresAt: number;
|
|
};
|
|
|
|
// Nettoyer les states expirés
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [state, data] of pendingStates.entries()) {
|
|
if (data.expiresAt < now) {
|
|
pendingStates.delete(state);
|
|
}
|
|
}
|
|
}, 60000); // Toutes les minutes
|
|
|
|
// Valider avec expiration
|
|
const pending = pendingStates.get(state);
|
|
if (!pending || pending.expiresAt < Date.now()) {
|
|
return res.status(400).json({ success: false, error: "State expired or invalid" });
|
|
}
|
|
```
|
|
|
|
#### 4. **Pas de protection contre les attaques par timing**
|
|
|
|
**Risque** : Les comparaisons de strings peuvent révéler des informations via timing attacks
|
|
**Recommandation** :
|
|
```typescript
|
|
import crypto from "crypto";
|
|
|
|
function constantTimeCompare(a: string, b: string): boolean {
|
|
if (a.length !== b.length) return false;
|
|
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
}
|
|
```
|
|
|
|
#### 5. **Endpoints sans authentification**
|
|
|
|
Tous les endpoints sont accessibles sans authentification utilisateur !
|
|
|
|
**Recommandation** :
|
|
```typescript
|
|
import { authenticate } from "../middleware/auth";
|
|
|
|
calendarRouter.use(authenticate); // Protéger toutes les routes
|
|
|
|
// Ou individuellement :
|
|
calendarRouter.get("/:profileId/connections", authenticate, (req, res) => {
|
|
// Vérifier que req.user.id === req.params.profileId
|
|
if (req.user.id !== req.params.profileId) {
|
|
return res.status(403).json({ error: "Forbidden" });
|
|
}
|
|
// ...
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 🛡️ Fiabilité : 4/10 ⚠️
|
|
|
|
### ❌ Points faibles
|
|
|
|
#### 1. **Gestion d'erreurs insuffisante**
|
|
```typescript
|
|
catch (e) {
|
|
console.error("OAuth start error:", e);
|
|
res.status(400).json({ message: "Invalid request" }); // Trop générique
|
|
}
|
|
```
|
|
|
|
**Problème** :
|
|
- Messages d'erreur génériques
|
|
- Pas de distinction entre erreurs client/serveur
|
|
- Logs insuffisants
|
|
- Pas de monitoring
|
|
|
|
**Recommandation** :
|
|
```typescript
|
|
import { ZodError } from "zod";
|
|
import { logger } from "../utils/logger";
|
|
|
|
try {
|
|
// ...
|
|
} catch (e) {
|
|
if (e instanceof ZodError) {
|
|
logger.warn("Validation error", { errors: e.errors, endpoint: req.path });
|
|
return res.status(400).json({
|
|
error: "Validation error",
|
|
details: e.errors.map(err => ({
|
|
field: err.path.join("."),
|
|
message: err.message
|
|
}))
|
|
});
|
|
}
|
|
|
|
logger.error("OAuth start failed", { error: e, profileId: req.body.profileId });
|
|
return res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
```
|
|
|
|
#### 2. **Pas de retry logic**
|
|
|
|
Si l'API Google/Outlook échoue temporairement, pas de retry automatique.
|
|
|
|
**Recommandation** :
|
|
```typescript
|
|
async function fetchWithRetry(
|
|
url: string,
|
|
options: RequestInit,
|
|
maxRetries = 3
|
|
): Promise<Response> {
|
|
for (let i = 0; i < maxRetries; i++) {
|
|
try {
|
|
const response = await fetch(url, options);
|
|
if (response.ok || response.status < 500) return response;
|
|
} catch (e) {
|
|
if (i === maxRetries - 1) throw e;
|
|
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
|
|
}
|
|
}
|
|
throw new Error("Max retries exceeded");
|
|
}
|
|
```
|
|
|
|
#### 3. **Pas de validation des tokens reçus**
|
|
|
|
Après l'échange code → token, pas de validation de la structure du token.
|
|
|
|
**Recommandation** :
|
|
```typescript
|
|
const tokenResponseSchema = z.object({
|
|
access_token: z.string().min(1),
|
|
refresh_token: z.string().optional(),
|
|
expires_in: z.number().positive(),
|
|
token_type: z.literal("Bearer"),
|
|
scope: z.string()
|
|
});
|
|
|
|
const tokens = tokenResponseSchema.parse(await response.json());
|
|
```
|
|
|
|
#### 4. **Race conditions possibles**
|
|
|
|
Si deux requêtes modifient la même connexion simultanément :
|
|
```typescript
|
|
list[idx] = { ...list[idx], status: "connected", lastSyncedAt: now };
|
|
connectionsByProfile.set(profileId, list);
|
|
```
|
|
|
|
**Recommandation** : Utiliser une base de données avec transactions ACID
|
|
|
|
---
|
|
|
|
## ⚡ Performance : 5/10 ⚠️
|
|
|
|
### Points faibles
|
|
|
|
#### 1. **Pas de cache**
|
|
|
|
Chaque requête refait potentiellement le même appel API.
|
|
|
|
**Recommandation** :
|
|
```typescript
|
|
import NodeCache from "node-cache";
|
|
|
|
const connectionCache = new NodeCache({
|
|
stdTTL: 300, // 5 minutes
|
|
checkperiod: 60
|
|
});
|
|
|
|
calendarRouter.get("/:profileId/connections", (req, res) => {
|
|
const profileId = req.params.profileId;
|
|
const cacheKey = `connections:${profileId}`;
|
|
|
|
const cached = connectionCache.get(cacheKey);
|
|
if (cached) return res.json(cached);
|
|
|
|
const list = connectionsByProfile.get(profileId) ?? [];
|
|
connectionCache.set(cacheKey, list);
|
|
res.json(list);
|
|
});
|
|
```
|
|
|
|
#### 2. **Pas de pagination**
|
|
|
|
Si un utilisateur a 1000 connexions (peu probable mais possible), tout est retourné d'un coup.
|
|
|
|
**Recommandation** :
|
|
```typescript
|
|
calendarRouter.get("/:profileId/connections", (req, res) => {
|
|
const page = parseInt(req.query.page as string) || 1;
|
|
const limit = parseInt(req.query.limit as string) || 20;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const list = connectionsByProfile.get(profileId) ?? [];
|
|
const paginated = list.slice(skip, skip + limit);
|
|
|
|
res.json({
|
|
data: paginated,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total: list.length,
|
|
totalPages: Math.ceil(list.length / limit)
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
#### 3. **Pas de rate limiting**
|
|
|
|
Un attaquant peut spam les endpoints OAuth.
|
|
|
|
**Recommandation** :
|
|
```typescript
|
|
import rateLimit from "express-rate-limit";
|
|
|
|
const oauthLimiter = rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 5, // 5 tentatives max
|
|
message: "Trop de tentatives OAuth, réessayez plus tard"
|
|
});
|
|
|
|
calendarRouter.post("/:provider/oauth/start", oauthLimiter, (req, res) => {
|
|
// ...
|
|
});
|
|
```
|
|
|
|
#### 4. **Synchronisation bloquante**
|
|
|
|
Le endpoint `/refresh` pourrait bloquer si l'API externe est lente.
|
|
|
|
**Recommandation** :
|
|
```typescript
|
|
// Synchronisation asynchrone avec job queue
|
|
import Bull from "bull";
|
|
|
|
const syncQueue = new Bull("calendar-sync");
|
|
|
|
syncQueue.process(async (job) => {
|
|
const { profileId, connectionId } = job.data;
|
|
// Faire la sync ici
|
|
});
|
|
|
|
calendarRouter.post("/:profileId/connections/:connectionId/refresh", async (req, res) => {
|
|
const job = await syncQueue.add({
|
|
profileId: req.params.profileId,
|
|
connectionId: req.params.connectionId
|
|
});
|
|
|
|
res.json({ jobId: job.id, status: "queued" });
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## ✨ Qualité du code : 6/10
|
|
|
|
### ✅ Points positifs
|
|
|
|
1. **Utilisation de Zod pour la validation** (lignes 26-34)
|
|
2. **Types TypeScript bien définis** (lignes 6-19)
|
|
3. **Séparation des concerns** (Router séparé)
|
|
4. **Code lisible** et bien structuré
|
|
|
|
### ❌ Points à améliorer
|
|
|
|
#### 1. **Pas de tests**
|
|
|
|
**Recommandation** :
|
|
```typescript
|
|
// calendar.test.ts
|
|
import request from "supertest";
|
|
import { app } from "../app";
|
|
|
|
describe("Calendar OAuth", () => {
|
|
describe("POST /:provider/oauth/start", () => {
|
|
it("should return auth URL for Google", async () => {
|
|
const response = await request(app)
|
|
.post("/api/calendar/google/oauth/start")
|
|
.send({
|
|
profileId: "test-user",
|
|
state: "random-state-123"
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.authUrl).toContain("accounts.google.com");
|
|
expect(response.body.authUrl).toContain("response_type=code");
|
|
});
|
|
|
|
it("should reject invalid provider", async () => {
|
|
const response = await request(app)
|
|
.post("/api/calendar/invalid/oauth/start")
|
|
.send({
|
|
profileId: "test-user",
|
|
state: "random-state-123"
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
#### 2. **Manque de documentation**
|
|
|
|
**Recommandation** :
|
|
```typescript
|
|
/**
|
|
* Démarre le flow OAuth pour un provider de calendrier
|
|
*
|
|
* @route POST /api/calendar/:provider/oauth/start
|
|
* @param provider - "google" ou "outlook"
|
|
* @body profileId - ID de l'utilisateur
|
|
* @body state - Token CSRF (min 6 caractères)
|
|
* @returns authUrl - URL de redirection OAuth
|
|
* @returns state - Le state pour vérification ultérieure
|
|
*
|
|
* @example
|
|
* POST /api/calendar/google/oauth/start
|
|
* {
|
|
* "profileId": "user-123",
|
|
* "state": "csrf-token-abc123"
|
|
* }
|
|
*
|
|
* Response:
|
|
* {
|
|
* "authUrl": "https://accounts.google.com/o/oauth2/v2/auth?...",
|
|
* "state": "csrf-token-abc123"
|
|
* }
|
|
*/
|
|
calendarRouter.post("/:provider/oauth/start", (req, res) => {
|
|
// ...
|
|
});
|
|
```
|
|
|
|
#### 3. **Constantes magiques**
|
|
|
|
```typescript
|
|
const { profileId, state } = oauthStartBody.parse(req.body ?? {}); // Pourquoi {} par défaut ?
|
|
```
|
|
|
|
**Recommandation** :
|
|
```typescript
|
|
const DEFAULT_REQUEST_BODY = {};
|
|
const MIN_STATE_LENGTH = 6;
|
|
|
|
const oauthStartBody = z.object({
|
|
profileId: z.string().min(1),
|
|
state: z.string().min(MIN_STATE_LENGTH, "State must be at least 6 characters")
|
|
});
|
|
```
|
|
|
|
#### 4. **Duplication de code**
|
|
|
|
La construction de l'URL OAuth pour Google et Outlook est similaire.
|
|
|
|
**Recommandation** :
|
|
```typescript
|
|
function buildOAuthUrl(
|
|
provider: CalendarProvider,
|
|
config: typeof OAUTH_CONFIG[CalendarProvider],
|
|
state: string
|
|
): string {
|
|
const baseUrls = {
|
|
google: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
outlook: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
|
|
};
|
|
|
|
const commonParams = {
|
|
client_id: config.clientId,
|
|
redirect_uri: config.redirectUri,
|
|
response_type: "code",
|
|
scope: config.scope,
|
|
state
|
|
};
|
|
|
|
const providerParams = provider === "google"
|
|
? { ...commonParams, access_type: "offline", prompt: "consent" }
|
|
: { ...commonParams, response_mode: "query" };
|
|
|
|
return `${baseUrls[provider]}?${new URLSearchParams(providerParams).toString()}`;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Import planning (IA, Google Agenda, Outlook)
|
|
|
|
### Architecture recommandée
|
|
|
|
```typescript
|
|
// services/calendar-import.service.ts
|
|
|
|
export class CalendarImportService {
|
|
/**
|
|
* Importe les événements depuis un provider
|
|
*/
|
|
async importEvents(
|
|
connectionId: string,
|
|
options: ImportOptions
|
|
): Promise<ImportResult> {
|
|
const connection = await this.getConnection(connectionId);
|
|
|
|
switch (connection.provider) {
|
|
case "google":
|
|
return this.importFromGoogle(connection, options);
|
|
case "outlook":
|
|
return this.importFromOutlook(connection, options);
|
|
default:
|
|
throw new Error(`Unsupported provider: ${connection.provider}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Importe depuis Google Calendar API
|
|
*/
|
|
private async importFromGoogle(
|
|
connection: ConnectedCalendar,
|
|
options: ImportOptions
|
|
): Promise<ImportResult> {
|
|
const accessToken = await this.getAccessToken(connection);
|
|
|
|
const response = await fetch(
|
|
`https://www.googleapis.com/calendar/v3/calendars/primary/events?${new URLSearchParams({
|
|
timeMin: options.startDate.toISOString(),
|
|
timeMax: options.endDate.toISOString(),
|
|
maxResults: String(options.maxResults || 250),
|
|
singleEvents: "true",
|
|
orderBy: "startTime"
|
|
})}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
"Content-Type": "application/json"
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
// Token expiré, refresh
|
|
await this.refreshToken(connection);
|
|
return this.importFromGoogle(connection, options); // Retry
|
|
}
|
|
throw new Error(`Google API error: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return this.normalizeGoogleEvents(data.items);
|
|
}
|
|
|
|
/**
|
|
* Importe depuis Microsoft Graph API
|
|
*/
|
|
private async importFromOutlook(
|
|
connection: ConnectedCalendar,
|
|
options: ImportOptions
|
|
): Promise<ImportResult> {
|
|
const accessToken = await this.getAccessToken(connection);
|
|
|
|
const response = await fetch(
|
|
`https://graph.microsoft.com/v1.0/me/calendar/events?${new URLSearchParams({
|
|
$filter: `start/dateTime ge '${options.startDate.toISOString()}' and end/dateTime le '${options.endDate.toISOString()}'`,
|
|
$top: String(options.maxResults || 250),
|
|
$orderby: "start/dateTime"
|
|
})}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
"Content-Type": "application/json"
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
await this.refreshToken(connection);
|
|
return this.importFromOutlook(connection, options);
|
|
}
|
|
throw new Error(`Microsoft API error: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return this.normalizeOutlookEvents(data.value);
|
|
}
|
|
|
|
/**
|
|
* Normalise les événements Google vers format commun
|
|
*/
|
|
private normalizeGoogleEvents(events: any[]): ImportResult {
|
|
const normalized = events.map(event => ({
|
|
id: generateUUID(),
|
|
externalId: event.id,
|
|
source: "google" as const,
|
|
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) => ({
|
|
email: a.email,
|
|
name: a.displayName,
|
|
status: a.responseStatus
|
|
})),
|
|
recurrence: event.recurrence,
|
|
createdAt: event.created,
|
|
updatedAt: event.updated
|
|
}));
|
|
|
|
return {
|
|
events: normalized,
|
|
count: normalized.length,
|
|
source: "google",
|
|
importedAt: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Normalise les événements Outlook vers format commun
|
|
*/
|
|
private normalizeOutlookEvents(events: any[]): ImportResult {
|
|
const normalized = events.map(event => ({
|
|
id: generateUUID(),
|
|
externalId: event.id,
|
|
source: "outlook" as const,
|
|
title: event.subject || "(Sans titre)",
|
|
description: event.bodyPreview,
|
|
startDate: event.start.dateTime,
|
|
endDate: event.end.dateTime,
|
|
isAllDay: event.isAllDay,
|
|
location: event.location?.displayName,
|
|
attendees: event.attendees?.map((a: any) => ({
|
|
email: a.emailAddress.address,
|
|
name: a.emailAddress.name,
|
|
status: a.status.response
|
|
})),
|
|
recurrence: event.recurrence,
|
|
createdAt: event.createdDateTime,
|
|
updatedAt: event.lastModifiedDateTime
|
|
}));
|
|
|
|
return {
|
|
events: normalized,
|
|
count: normalized.length,
|
|
source: "outlook",
|
|
importedAt: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Utilise l'IA pour enrichir les événements importés
|
|
*/
|
|
async enrichWithAI(events: NormalizedEvent[]): Promise<EnrichedEvent[]> {
|
|
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
|
|
return Promise.all(events.map(async (event) => {
|
|
const prompt = `
|
|
Analyse cet événement de calendrier et catégorise-le :
|
|
|
|
Titre: ${event.title}
|
|
Description: ${event.description || "N/A"}
|
|
Lieu: ${event.location || "N/A"}
|
|
|
|
Retourne un JSON avec:
|
|
- category: "work" | "personal" | "family" | "school" | "health" | "other"
|
|
- priority: "low" | "medium" | "high"
|
|
- tags: string[] (max 5 tags pertinents)
|
|
- suggestedReminder: number (minutes avant l'événement, 0 si non pertinent)
|
|
`;
|
|
|
|
const response = await openai.chat.completions.create({
|
|
model: "gpt-4o-mini",
|
|
messages: [{ role: "user", content: prompt }],
|
|
response_format: { type: "json_object" }
|
|
});
|
|
|
|
const aiEnrichment = JSON.parse(response.choices[0].message.content);
|
|
|
|
return {
|
|
...event,
|
|
...aiEnrichment,
|
|
confidence: 0.85 // Confiance dans la catégorisation IA
|
|
};
|
|
}));
|
|
}
|
|
}
|
|
|
|
// Types
|
|
type ImportOptions = {
|
|
startDate: Date;
|
|
endDate: Date;
|
|
maxResults?: number;
|
|
};
|
|
|
|
type ImportResult = {
|
|
events: NormalizedEvent[];
|
|
count: number;
|
|
source: "google" | "outlook";
|
|
importedAt: string;
|
|
};
|
|
|
|
type NormalizedEvent = {
|
|
id: string;
|
|
externalId: string;
|
|
source: "google" | "outlook";
|
|
title: string;
|
|
description?: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
isAllDay: boolean;
|
|
location?: string;
|
|
attendees?: Array<{
|
|
email: string;
|
|
name?: string;
|
|
status?: string;
|
|
}>;
|
|
recurrence?: any;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
type EnrichedEvent = NormalizedEvent & {
|
|
category: "work" | "personal" | "family" | "school" | "health" | "other";
|
|
priority: "low" | "medium" | "high";
|
|
tags: string[];
|
|
suggestedReminder: number;
|
|
confidence: number;
|
|
};
|
|
```
|
|
|
|
### Endpoint d'import
|
|
|
|
```typescript
|
|
// routes/calendar.ts (à ajouter)
|
|
|
|
calendarRouter.post("/:profileId/connections/:connectionId/import", async (req, res) => {
|
|
try {
|
|
const { profileId, connectionId } = req.params;
|
|
const { startDate, endDate, enrichWithAI = false } = req.body;
|
|
|
|
const importService = new CalendarImportService();
|
|
|
|
let result = await importService.importEvents(connectionId, {
|
|
startDate: new Date(startDate),
|
|
endDate: new Date(endDate)
|
|
});
|
|
|
|
if (enrichWithAI) {
|
|
result.events = await importService.enrichWithAI(result.events);
|
|
}
|
|
|
|
// Sauvegarder dans la base
|
|
await saveEventsToDatabase(profileId, result.events);
|
|
|
|
res.json({
|
|
success: true,
|
|
imported: result.count,
|
|
enriched: enrichWithAI,
|
|
importedAt: result.importedAt
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error("Import failed", { error, profileId, connectionId });
|
|
res.status(500).json({ error: "Import failed" });
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 📈 Score global et recommandations
|
|
|
|
| Critère | Score | Priorité amélioration |
|
|
|---------|-------|----------------------|
|
|
| **Sécurité** | 3/10 | 🔴 CRITIQUE |
|
|
| **Fiabilité** | 4/10 | 🟠 HAUTE |
|
|
| **Performance** | 5/10 | 🟡 MOYENNE |
|
|
| **Qualité code** | 6/10 | 🟡 MOYENNE |
|
|
| **Import/Sync** | N/A | 🔴 À IMPLÉMENTER |
|
|
|
|
### Roadmap d'amélioration
|
|
|
|
#### Phase 1 - Sécurité CRITIQUE (1 semaine)
|
|
1. ✅ Implémenter chiffrement tokens (AES-256-GCM)
|
|
2. ✅ Ajouter validation stricte variables d'environnement
|
|
3. ✅ Implémenter expiration des states OAuth
|
|
4. ✅ Ajouter middleware d'authentification
|
|
5. ✅ Révoquer clé OpenAI exposée
|
|
|
|
#### Phase 2 - Fiabilité (1 semaine)
|
|
1. ✅ Migrer vers base de données (PostgreSQL + Prisma)
|
|
2. ✅ Implémenter gestion d'erreurs robuste
|
|
3. ✅ Ajouter retry logic avec backoff exponentiel
|
|
4. ✅ Implémenter logging structuré (Winston/Pino)
|
|
5. ✅ Ajouter monitoring (Sentry/DataDog)
|
|
|
|
#### Phase 3 - Import/Synchronisation (2 semaines)
|
|
1. ✅ Implémenter échange code OAuth → tokens
|
|
2. ✅ Implémenter refresh token automatique
|
|
3. ✅ Créer CalendarImportService
|
|
4. ✅ Normaliser événements Google/Outlook
|
|
5. ✅ Intégrer enrichissement IA (optionnel)
|
|
6. ✅ Implémenter sync incrémentale (webhook si possible)
|
|
|
|
#### Phase 4 - Performance (1 semaine)
|
|
1. ✅ Ajouter cache Redis
|
|
2. ✅ Implémenter pagination
|
|
3. ✅ Ajouter rate limiting
|
|
4. ✅ Créer job queue pour sync asynchrone
|
|
5. ✅ Optimiser requêtes base de données
|
|
|
|
#### Phase 5 - Qualité (1 semaine)
|
|
1. ✅ Écrire tests unitaires (Jest)
|
|
2. ✅ Écrire tests d'intégration (Supertest)
|
|
3. ✅ Ajouter documentation OpenAPI/Swagger
|
|
4. ✅ Refactorer code dupliqué
|
|
5. ✅ Setup CI/CD avec tests
|
|
|
|
---
|
|
|
|
## 💡 Conclusion
|
|
|
|
Le code actuel est un **prototype fonctionnel** mais **NON PRODUCTION-READY**.
|
|
|
|
**Forces** :
|
|
- Structure claire
|
|
- Utilisation de TypeScript
|
|
- Validation avec Zod
|
|
- Correction de l'erreur OAuth "response_type"
|
|
|
|
**Faiblesses critiques** :
|
|
- Aucune sécurité (tokens en clair, pas d'auth)
|
|
- Pas de persistance (perte au redémarrage)
|
|
- Pas de gestion d'erreurs robuste
|
|
- Pas de tests
|
|
- Import/sync calendrier non implémenté
|
|
|
|
**Temps estimé pour production** : 6-8 semaines avec 1 développeur full-time
|
|
|
|
**Priorité immédiate** :
|
|
1. 🔴 Chiffrement tokens
|
|
2. 🔴 Base de données
|
|
3. 🟠 Implémentation OAuth complète
|
|
4. 🟠 Import Google/Outlook
|
|
5. 🟡 Tests et monitoring
|