diff --git a/.DS_Store b/.DS_Store index 60b378a6..354e791a 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/ACTION_PLAN_LOGIN_FLOW.md b/ACTION_PLAN_LOGIN_FLOW.md deleted file mode 100644 index 7c93d3bc..00000000 --- a/ACTION_PLAN_LOGIN_FLOW.md +++ /dev/null @@ -1,425 +0,0 @@ -# Plan d'Action - Amélioration Flow de Connexion - -## 🎯 Objectifs - -1. **Améliorer l'UX** : Permettre SSO naturel pour les utilisateurs légitimes -2. **Sécuriser le logout** : S'assurer que les credentials sont demandés après logout -3. **Simplifier le code** : Réduire la complexité de détection session invalide -4. **Éliminer les race conditions** : Mécanisme robuste pour éviter auto-login après logout - ---- - -## 📋 Actions Immédiates (À faire en premier) - -### Action 1 : Supprimer `prompt=login` par défaut ⚡ - -**Fichier** : `app/api/auth/options.ts` - -**Changement** : -```typescript -// AVANT (ligne 147-155) -authorization: { - params: { - scope: "openid profile email roles", - prompt: "login" // ❌ Supprimer cette ligne - } -} - -// APRÈS -authorization: { - params: { - scope: "openid profile email roles", - // prompt: "login" supprimé - sera ajouté conditionnellement après logout - } -} -``` - -**Impact** : ✅ SSO fonctionne naturellement pour les utilisateurs légitimes - ---- - -### Action 2 : Créer route API pour marquer logout ⚡ - -**Nouveau fichier** : `app/api/auth/mark-logout/route.ts` - -```typescript -import { NextRequest, NextResponse } from 'next/server'; - -export async function POST(request: NextRequest) { - const response = NextResponse.json({ - success: true, - message: 'Logout marked successfully' - }); - - // Cookie HttpOnly pour marquer le logout (5 minutes) - response.cookies.set('force_login_prompt', 'true', { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/', - maxAge: 300 // 5 minutes - }); - - return response; -} -``` - -**Impact** : ✅ Mécanisme robuste pour forcer login après logout - ---- - -### Action 3 : Modifier signout-handler pour utiliser la route ⚡ - -**Fichier** : `components/auth/signout-handler.tsx` - -**Changement** (après ligne 25) : -```typescript -// AVANT -clearKeycloakCookies(); - -// APRÈS -clearKeycloakCookies(); - -// Marquer le logout côté serveur -try { - await fetch('/api/auth/mark-logout', { - method: 'POST', - credentials: 'include', - }); -} catch (error) { - console.error('Error marking logout:', error); - // Continue même si ça échoue -} -``` - -**Répéter dans** : -- `components/main-nav.tsx` (ligne ~377) -- `components/layout/layout-wrapper.tsx` (ligne ~42) - -**Impact** : ✅ Flag serveur pour empêcher auto-login - ---- - -### Action 4 : Simplifier signin/page.tsx ⚡ - -**Fichier** : `app/signin/page.tsx` - -**Changement** : Remplacer la logique complexe (lignes 17-67) par : - -```typescript -useEffect(() => { - // Vérifier le cookie serveur pour forcer login - const forceLoginCookie = document.cookie - .split(';') - .find(c => c.trim().startsWith('force_login_prompt=')); - - // Si logout récent, forcer prompt=login - if (forceLoginCookie) { - // Supprimer le cookie - document.cookie = 'force_login_prompt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - - // Ne pas auto-login, attendre clic utilisateur - // Le bouton "Se connecter" forcera prompt=login - return; - } - - // Si déjà authentifié, rediriger - if (status === "authenticated" && session?.user) { - router.push("/"); - return; - } - - // Si non authentifié et pas de flag logout, auto-login (SSO naturel) - if (status === "unauthenticated" && !forceLoginCookie) { - const timer = setTimeout(() => { - if (status === "unauthenticated") { - signIn("keycloak", { callbackUrl: "/" }); - } - }, 1000); - return () => clearTimeout(timer); - } -}, [status, session, router]); -``` - -**ET** modifier le bouton "Se connecter" (ligne ~202) : - -```typescript - -``` - -**Impact** : ✅ Code plus simple et maintenable - ---- - -### Action 5 : Ajouter prompt=login conditionnel dans options.ts ⚡ - -**Fichier** : `app/api/auth/options.ts` - -**Changement** : Modifier la configuration KeycloakProvider pour accepter un paramètre custom : - -```typescript -KeycloakProvider({ - clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"), - clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"), - issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"), - authorization: { - params: { - scope: "openid profile email roles", - // prompt sera ajouté dynamiquement si force_login=true dans l'URL - } - }, - // ... profile callback ... -}) -``` - -**ET** créer une route custom pour signin qui ajoute prompt : - -**Nouveau fichier** : `app/api/auth/signin/keycloak/route.ts` - -```typescript -import { NextRequest, NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth/next'; -import { authOptions } from '../../options'; - -export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const forceLogin = searchParams.get('force_login') === 'true'; - const callbackUrl = searchParams.get('callbackUrl') || '/'; - - // Rediriger vers NextAuth signin avec prompt si nécessaire - const signinUrl = new URL('/api/auth/signin/keycloak', request.nextUrl.origin); - signinUrl.searchParams.set('callbackUrl', callbackUrl); - - if (forceLogin) { - // Ajouter prompt=login dans l'URL de redirection Keycloak - // Note: NextAuth ne supporte pas directement, il faut modifier l'URL après - // Solution alternative: Utiliser un middleware ou modifier options dynamiquement - } - - return NextResponse.redirect(signinUrl); -} -``` - -**OU** Solution plus simple : Modifier directement dans `options.ts` pour lire un cookie : - -```typescript -// Dans options.ts, modifier authorization params dynamiquement -authorization: { - params: (provider, action, request) => { - const forceLogin = request?.cookies?.get('force_login_prompt')?.value === 'true'; - return { - scope: "openid profile email roles", - ...(forceLogin ? { prompt: "login" } : {}), - }; - } -} -``` - -**Note** : NextAuth v4 ne supporte pas `params` comme fonction. Solution alternative : - -**Modifier** `app/api/auth/options.ts` pour utiliser `authorization.url` : - -```typescript -KeycloakProvider({ - // ... config ... - authorization: { - params: { - scope: "openid profile email roles", - }, - // Ajouter prompt dynamiquement via URL personnalisée - url: (params) => { - // Vérifier si on doit forcer login (via cookie ou autre moyen) - const url = new URL(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/auth`); - url.searchParams.set('client_id', process.env.KEYCLOAK_CLIENT_ID!); - url.searchParams.set('redirect_uri', params.redirect_uri); - url.searchParams.set('response_type', 'code'); - url.searchParams.set('scope', 'openid profile email roles'); - url.searchParams.set('state', params.state); - - // Ajouter prompt si nécessaire (à vérifier via cookie dans le callback) - // Note: Plus complexe, nécessite de passer le flag via state - return url.toString(); - } - } -}) -``` - -**Solution RECOMMANDÉE (plus simple)** : Utiliser un paramètre dans l'URL de callback et le vérifier dans le callback JWT : - -```typescript -// Dans signin/page.tsx, lors du clic sur "Se connecter" -const url = new URL(window.location.origin + '/api/auth/signin/keycloak'); -url.searchParams.set('callbackUrl', '/'); -url.searchParams.set('force_login', 'true'); -// Stocker dans sessionStorage pour le callback -sessionStorage.setItem('force_login', 'true'); -window.location.href = url.toString(); - -// Dans options.ts, callback jwt, vérifier sessionStorage n'est pas possible côté serveur -// Solution: Passer via state OAuth -``` - -**MEILLEURE SOLUTION** : Utiliser un cookie avant le signIn : - -```typescript -// Dans signin/page.tsx, bouton "Se connecter" -onClick={() => { - // Créer cookie pour forcer login - document.cookie = 'force_login_prompt=true; path=/; max-age=300'; - // Puis signIn normal - signIn("keycloak", { callbackUrl: "/" }); -}} - -// Dans options.ts, lire le cookie dans authorization params -// Note: NextAuth ne permet pas d'accéder aux cookies dans params -// Solution: Middleware ou route custom -``` - -**SOLUTION FINALE RECOMMANDÉE** : Créer une route API custom qui gère le signin avec prompt conditionnel : - -```typescript -// app/api/auth/custom-signin/route.ts -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const forceLogin = searchParams.get('force_login') === 'true'; - const callbackUrl = searchParams.get('callbackUrl') || '/'; - - // Construire l'URL Keycloak avec prompt si nécessaire - const keycloakIssuer = process.env.KEYCLOAK_ISSUER!; - const clientId = process.env.KEYCLOAK_CLIENT_ID!; - const redirectUri = `${request.nextUrl.origin}/api/auth/callback/keycloak`; - - const authUrl = new URL(`${keycloakIssuer}/protocol/openid-connect/auth`); - authUrl.searchParams.set('client_id', clientId); - authUrl.searchParams.set('redirect_uri', redirectUri); - authUrl.searchParams.set('response_type', 'code'); - authUrl.searchParams.set('scope', 'openid profile email roles'); - authUrl.searchParams.set('state', generateState()); // Générer state - - if (forceLogin) { - authUrl.searchParams.set('prompt', 'login'); - } - - return NextResponse.redirect(authUrl.toString()); -} -``` - -**Impact** : ✅ Prompt login seulement après logout - ---- - -## 🔧 Actions Secondaires (Après les actions immédiates) - -### Action 6 : Configurer explicitement les cookies NextAuth - -**Fichier** : `app/api/auth/options.ts` - -**Ajouter** après `session: { ... }` : - -```typescript -cookies: { - sessionToken: { - name: `next-auth.session-token`, - options: { - httpOnly: true, - sameSite: 'lax', - path: '/', - secure: process.env.NEXTAUTH_URL?.startsWith('https://') ?? false, - }, - }, - // ... autres cookies si nécessaire -}, -``` - ---- - -### Action 7 : Améliorer Keycloak logout URL - -**Fichiers** : -- `components/auth/signout-handler.tsx` -- `components/main-nav.tsx` -- `components/layout/layout-wrapper.tsx` - -**Changement** (ligne ~58-76) : - -```typescript -// AVANT -keycloakLogoutUrl.searchParams.append('kc_action', 'LOGOUT'); - -// APRÈS -keycloakLogoutUrl.searchParams.append('kc_action', 'LOGOUT'); -// Ajouter client_id pour forcer logout client spécifique -if (process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID) { - keycloakLogoutUrl.searchParams.append('client_id', - process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID); -} -``` - ---- - -### Action 8 : Améliorer gestion erreur refresh token - -**Fichier** : `app/api/auth/options.ts` - -**Changement** : Voir détails dans `IMPROVEMENTS_LOGIN_FLOW.md` section "Problème 7" - ---- - -## ✅ Checklist d'Implémentation - -### Phase 1 : Corrections Critiques (1-2 heures) -- [ ] Action 1 : Supprimer `prompt=login` par défaut -- [ ] Action 2 : Créer route `/api/auth/mark-logout` -- [ ] Action 3 : Modifier signout-handler pour utiliser la route -- [ ] Action 4 : Simplifier signin/page.tsx -- [ ] Action 5 : Ajouter prompt=login conditionnel - -### Phase 2 : Améliorations (1 heure) -- [ ] Action 6 : Configurer explicitement les cookies -- [ ] Action 7 : Améliorer Keycloak logout URL -- [ ] Action 8 : Améliorer gestion erreur refresh - -### Phase 3 : Tests (30 minutes) -- [ ] Tester login première visite (SSO doit fonctionner) -- [ ] Tester login après logout (credentials doivent être demandés) -- [ ] Tester logout depuis dashboard -- [ ] Tester logout depuis iframe -- [ ] Tester expiration session - ---- - -## 🎯 Résultat Attendu - -### Avant -- ❌ Toujours demander credentials (même première visite) -- ❌ Logique complexe de détection session invalide -- ❌ Race conditions possibles -- ❌ Cookies Keycloak peuvent persister - -### Après -- ✅ SSO naturel pour utilisateurs légitimes -- ✅ Credentials demandés seulement après logout -- ✅ Détection session invalide simple et robuste -- ✅ Pas de race conditions -- ✅ Meilleure gestion des cookies - ---- - -**Document créé le** : $(date) -**Priorité** : Actions immédiates à faire en premier - diff --git a/AUDIT_API_N8N_CONNECTION.md b/AUDIT_API_N8N_CONNECTION.md new file mode 100644 index 00000000..e97bc943 --- /dev/null +++ b/AUDIT_API_N8N_CONNECTION.md @@ -0,0 +1,724 @@ +# 🔍 Audit Développeur Senior - Connexion API Next.js ↔️ N8N (Missions) + +**Date**: $(date) +**Auteur**: Audit Développeur Senior +**Objectif**: Vérifier et documenter la connexion entre Next.js et N8N pour la gestion des missions + +--- + +## 📋 Table des Matières + +1. [Architecture Globale](#architecture-globale) +2. [Flux de Communication](#flux-de-communication) +3. [Endpoints API](#endpoints-api) +4. [Configuration Requise](#configuration-requise) +5. [Sécurité](#sécurité) +6. [Points Critiques à Vérifier](#points-critiques-à-vérifier) +7. [Problèmes Potentiels et Solutions](#problèmes-potentiels-et-solutions) +8. [Tests et Validation](#tests-et-validation) +9. [Recommandations](#recommandations) + +--- + +## 🏗️ Architecture Globale + +### Vue d'ensemble + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Next.js │────────▶│ N8N │────────▶│ Intégrations│ +│ (API) │ │ (Workflow) │ │ (Gitea, etc)│ +└─────────────┘ └─────────────┘ └─────────────┘ + │ │ + │ │ + └─────────────────────────┘ + (Callback) +``` + +### Composants Principaux + +1. **Next.js API Routes** + - `POST /api/missions` - Création de mission + - `POST /api/missions/mission-created` - Callback de N8N + - `GET /api/missions` - Liste des missions + +2. **Service N8N** (`lib/services/n8n-service.ts`) + - Envoi de données vers N8N + - Gestion des webhooks + - Gestion des erreurs + +3. **N8N Workflows** + - Webhook de réception: `/webhook/mission-created` + - Création des intégrations externes + - Callback vers Next.js + +--- + +## 🔄 Flux de Communication + +### 1. Création d'une Mission (Next.js → N8N → Next.js) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ÉTAPE 1: Création Mission dans Next.js │ +└─────────────────────────────────────────────────────────────────┘ +POST /api/missions + ↓ +1. Validation des données +2. Création en base de données (Prisma) +3. Upload des fichiers (logo, attachments) vers Minio +4. Vérification des fichiers + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ ÉTAPE 2: Envoi vers N8N │ +└─────────────────────────────────────────────────────────────────┘ +POST https://brain.slm-lab.net/webhook/mission-created +Headers: + - Content-Type: application/json + - x-api-key: {N8N_API_KEY} +Body: + { + missionId: "uuid", + name: "...", + oddScope: [...], + services: [...], + config: { + N8N_API_KEY: "...", + MISSION_API_URL: "https://api.slm-lab.net/api" + }, + ... + } + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ ÉTAPE 3: Traitement N8N │ +└─────────────────────────────────────────────────────────────────┘ +N8N Workflow: + 1. Réception webhook + 2. Création Gitea repository (si service "Gite") + 3. Création Leantime project (si service "Leantime") + 4. Création Outline collection (si service "Documentation") + 5. Création RocketChat channel (si service "RocketChat") + 6. Préparation des données de callback + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ ÉTAPE 4: Callback N8N → Next.js │ +└─────────────────────────────────────────────────────────────────┘ +POST {MISSION_API_URL}/api/missions/mission-created +Headers: + - Content-Type: application/json + - x-api-key: {N8N_API_KEY} (depuis config.N8N_API_KEY) +Body: + { + missionId: "uuid", + gitRepoUrl: "...", + leantimeProjectId: "...", + documentationCollectionId: "...", + rocketchatChannelId: "..." + } + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ ÉTAPE 5: Mise à jour Mission dans Next.js │ +└─────────────────────────────────────────────────────────────────┘ +Validation API key +Recherche mission par missionId +Mise à jour des champs d'intégration: + - giteaRepositoryUrl + - leantimeProjectId + - outlineCollectionId + - rocketChatChannelId +``` + +--- + +## 🔌 Endpoints API + +### 1. POST /api/missions + +**Fichier**: `app/api/missions/route.ts` + +**Fonction**: Créer une nouvelle mission et déclencher le workflow N8N + +**Authentification**: +- Session utilisateur requise (via `getServerSession`) +- Vérification: `checkAuth(request)` + +**Body attendu**: +```typescript +{ + name: string; + oddScope: string[]; + niveau?: string; + intention?: string; + missionType?: string; + services?: string[]; + guardians?: Record; + volunteers?: string[]; + logo?: { data: string; name?: string; type?: string }; + attachments?: Array<{ data: string; name?: string; type?: string }>; +} +``` + +**Réponse**: +```json +{ + "success": true, + "mission": { ... }, + "message": "Mission created successfully with all integrations" +} +``` + +**Points critiques**: +- ✅ Mission créée en base AVANT l'envoi à N8N +- ✅ Fichiers uploadés et vérifiés AVANT l'envoi à N8N +- ✅ `missionId` inclus dans les données envoyées à N8N +- ✅ `config.N8N_API_KEY` et `config.MISSION_API_URL` inclus + +--- + +### 2. POST /api/missions/mission-created + +**Fichier**: `app/api/missions/mission-created/route.ts` + +**Fonction**: Recevoir les IDs d'intégration de N8N et mettre à jour la mission + +**Authentification**: +- **API Key** via header `x-api-key` +- **PAS** de session utilisateur requise (N8N n'a pas de session) + +**Headers requis**: +``` +x-api-key: {N8N_API_KEY} +Content-Type: application/json +``` + +**Body attendu**: +```typescript +{ + missionId: string; // ✅ Préféré (plus fiable) + // OU (fallback pour compatibilité) + name: string; + creatorId: string; + + // IDs d'intégration (optionnels) + gitRepoUrl?: string; + leantimeProjectId?: string | number; + documentationCollectionId?: string; + rocketchatChannelId?: string; +} +``` + +**Réponse succès**: +```json +{ + "success": true, + "message": "Mission updated successfully", + "mission": { + "id": "...", + "name": "...", + "giteaRepositoryUrl": "...", + "leantimeProjectId": "...", + "outlineCollectionId": "...", + "rocketChatChannelId": "..." + } +} +``` + +**Codes d'erreur**: +- `401` - API key invalide ou manquante +- `400` - Champs requis manquants +- `404` - Mission non trouvée +- `500` - Erreur serveur + +**Points critiques**: +- ✅ Validation stricte de l'API key +- ✅ Recherche par `missionId` (préféré) ou `name + creatorId` (fallback) +- ✅ Conversion `leantimeProjectId` de number vers string si nécessaire +- ✅ Mise à jour uniquement des champs fournis + +--- + +## ⚙️ Configuration Requise + +### Variables d'Environnement + +#### 1. N8N_API_KEY (OBLIGATOIRE) +```env +N8N_API_KEY=LwgeE1ntADD20OuWC88S3pR0EaO7FtO4 +``` + +**Usage**: +- Envoyé à N8N dans `config.N8N_API_KEY` +- N8N l'utilise pour authentifier le callback +- Vérifié côté serveur dans `/api/missions/mission-created` + +**Où configurer**: +- `.env.local` (développement) +- Variables d'environnement production (CapRover, Vercel, Docker, etc.) + +**Vérification**: +```typescript +// Erreur si non défini +if (!process.env.N8N_API_KEY) { + logger.error('N8N_API_KEY is not set in environment variables'); +} +``` + +--- + +#### 2. N8N_WEBHOOK_URL (Optionnel) +```env +N8N_WEBHOOK_URL=https://brain.slm-lab.net/webhook/mission-created +``` + +**Valeur par défaut**: `https://brain.slm-lab.net/webhook/mission-created` + +**Usage**: URL du webhook N8N pour la création de mission + +--- + +#### 3. NEXT_PUBLIC_API_URL (Recommandé) +```env +NEXT_PUBLIC_API_URL=https://api.slm-lab.net/api +``` + +**Usage**: +- Envoyé à N8N dans `config.MISSION_API_URL` +- N8N l'utilise pour construire l'URL du callback +- Format attendu: `{MISSION_API_URL}/api/missions/mission-created` + +**Valeur par défaut**: `https://api.slm-lab.net/api` + +--- + +#### 4. N8N_ROLLBACK_WEBHOOK_URL (Optionnel) +```env +N8N_ROLLBACK_WEBHOOK_URL=https://brain.slm-lab.net/webhook/mission-rollback +``` + +**Usage**: URL du webhook N8N pour le rollback de mission + +--- + +### Configuration N8N Workflow + +#### Webhook de Réception + +**Path**: `mission-created` +**URL complète**: `https://brain.slm-lab.net/webhook/mission-created` +**Méthode**: `POST` +**Status**: Doit être **ACTIF** (toggle vert dans N8N) + +--- + +#### Node "Save Mission To API" + +**URL**: +``` +{{ $node['Process Mission Data'].json.config.MISSION_API_URL }}/api/missions/mission-created +``` + +**Méthode**: `POST` + +**Headers**: +``` +Content-Type: application/json +x-api-key: {{ $node['Process Mission Data'].json.config.N8N_API_KEY }} +``` + +**Body**: +```json +{ + "missionId": "{{ $node['Process Mission Data'].json.missionId }}", + "gitRepoUrl": "{{ $node['Create Git Repo'].json.url }}", + "leantimeProjectId": "{{ $node['Create Leantime Project'].json.id }}", + "documentationCollectionId": "{{ $node['Create Outline Collection'].json.id }}", + "rocketchatChannelId": "{{ $node['Create RocketChat Channel'].json.id }}" +} +``` + +**Points critiques**: +- ✅ Utiliser `config.MISSION_API_URL` (pas d'URL en dur) +- ✅ Utiliser `config.N8N_API_KEY` (pas de clé en dur) +- ✅ Inclure `missionId` dans le body +- ✅ Inclure tous les IDs d'intégration créés + +--- + +## 🔒 Sécurité + +### 1. Authentification API Key + +**Mécanisme**: +- N8N envoie `x-api-key` header +- Next.js compare avec `process.env.N8N_API_KEY` +- Si différent → `401 Unauthorized` + +**Code de validation** (`app/api/missions/mission-created/route.ts:42`): +```typescript +const apiKey = request.headers.get('x-api-key'); +const expectedApiKey = process.env.N8N_API_KEY; + +if (apiKey !== expectedApiKey) { + logger.error('Invalid API key', { + received: apiKey ? 'present' : 'missing', + expected: expectedApiKey ? 'configured' : 'missing' + }); + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); +} +``` + +**Points critiques**: +- ✅ Comparaison stricte (pas de hash, clé en clair) +- ✅ Logging des tentatives invalides +- ✅ Pas de fallback si clé manquante + +--- + +### 2. Transmission de la Clé API + +**Flux**: +1. Next.js lit `process.env.N8N_API_KEY` +2. Next.js envoie à N8N dans `config.N8N_API_KEY` +3. N8N stocke temporairement dans le workflow +4. N8N renvoie dans header `x-api-key` lors du callback + +**Risque**: Si `N8N_API_KEY` est `undefined` au moment de l'envoi: +- N8N reçoit `undefined` ou chaîne vide +- N8N envoie chaîne vide dans le header +- Next.js rejette avec `401` + +**Solution**: Vérifier que `N8N_API_KEY` est défini avant l'envoi à N8N + +--- + +### 3. Validation des Données + +**Côté Next.js**: +- ✅ Validation des champs requis +- ✅ Recherche de mission par `missionId` (plus sûr que `name + creatorId`) +- ✅ Conversion de types (number → string pour `leantimeProjectId`) + +**Côté N8N**: +- ⚠️ Pas de validation visible dans le code Next.js +- ⚠️ N8N doit valider les données avant création des intégrations + +--- + +## ⚠️ Points Critiques à Vérifier + +### 1. Configuration Environnement + +- [ ] `N8N_API_KEY` est défini dans l'environnement +- [ ] `N8N_API_KEY` a la même valeur partout (dev, staging, prod) +- [ ] `NEXT_PUBLIC_API_URL` pointe vers la bonne URL +- [ ] Application redémarrée après modification des variables + +--- + +### 2. Workflow N8N + +- [ ] Workflow est **ACTIF** (toggle vert) +- [ ] Webhook path est correct: `mission-created` +- [ ] Node "Save Mission To API" utilise `config.MISSION_API_URL` +- [ ] Node "Save Mission To API" utilise `config.N8N_API_KEY` +- [ ] Node "Save Mission To API" inclut `missionId` dans le body +- [ ] Tous les IDs d'intégration sont inclus dans le callback + +--- + +### 3. Flux de Données + +- [ ] `missionId` est envoyé à N8N lors de la création +- [ ] `missionId` est renvoyé par N8N dans le callback +- [ ] Les IDs d'intégration sont correctement mappés: + - `gitRepoUrl` → `giteaRepositoryUrl` + - `leantimeProjectId` → `leantimeProjectId` (string) + - `documentationCollectionId` → `outlineCollectionId` + - `rocketchatChannelId` → `rocketChatChannelId` + +--- + +### 4. Gestion d'Erreurs + +- [ ] Erreurs N8N sont loggées +- [ ] Rollback en cas d'échec (si configuré) +- [ ] Messages d'erreur clairs pour debugging +- [ ] Pas de données sensibles dans les logs + +--- + +## 🐛 Problèmes Potentiels et Solutions + +### Problème 1: 401 Unauthorized + +**Symptômes**: +``` +Invalid API key { received: 'present', expected: 'configured' } +``` + +**Causes possibles**: +1. `N8N_API_KEY` non défini dans l'environnement +2. `N8N_API_KEY` différent entre Next.js et N8N +3. N8N envoie une clé vide ou `undefined` + +**Solutions**: +1. Vérifier que `N8N_API_KEY` est défini: + ```bash + echo $N8N_API_KEY + ``` +2. Vérifier la valeur dans N8N: + - Ouvrir l'exécution du workflow + - Vérifier `config.N8N_API_KEY` dans "Process Mission Data" +3. S'assurer que la même clé est utilisée partout + +--- + +### Problème 2: 404 Mission Not Found + +**Symptômes**: +``` +Mission not found { missionId: "...", name: "...", creatorId: "..." } +``` + +**Causes possibles**: +1. `missionId` non envoyé par N8N +2. `missionId` incorrect +3. Mission supprimée entre temps + +**Solutions**: +1. Vérifier que N8N envoie `missionId`: + ```json + { + "missionId": "{{ $node['Process Mission Data'].json.missionId }}" + } + ``` +2. Vérifier que Next.js envoie `missionId` à N8N: + ```typescript + config: { + missionId: mission.id // ✅ Inclus dans n8nData + } + ``` +3. Utiliser le fallback `name + creatorId` si `missionId` manquant + +--- + +### Problème 3: 500 Server Configuration Error + +**Symptômes**: +``` +N8N_API_KEY not configured in environment +``` + +**Cause**: `process.env.N8N_API_KEY` est `undefined` + +**Solution**: +1. Ajouter `N8N_API_KEY` à `.env.local` ou variables d'environnement +2. Redémarrer l'application +3. Vérifier avec un endpoint de test + +--- + +### Problème 4: 404 Webhook Not Registered + +**Symptômes**: +``` +404 Error: The requested webhook "mission-created" is not registered. +Hint: Click the 'Execute workflow' button on the canvas, then try again. +``` + +**Cause**: Workflow N8N n'est pas actif + +**Solution**: +1. Ouvrir le workflow dans N8N +2. Activer le toggle "Active" (devrait être vert) +3. Vérifier que le webhook node est actif + +--- + +### Problème 5: IDs d'Intégration Non Sauvegardés + +**Symptômes**: +- Mission créée mais `giteaRepositoryUrl`, `leantimeProjectId`, etc. sont `null` + +**Causes possibles**: +1. N8N ne rappelle pas `/api/missions/mission-created` +2. N8N rappelle mais avec des IDs manquants +3. Erreur lors de la mise à jour en base + +**Solutions**: +1. Vérifier les logs N8N (Executions) +2. Vérifier que le node "Save Mission To API" s'exécute +3. Vérifier les logs Next.js pour "Mission Created Webhook Received" +4. Vérifier que tous les IDs sont inclus dans le body du callback + +--- + +## 🧪 Tests et Validation + +### Test 1: Vérifier Configuration + +**Endpoint de test** (à créer): +```typescript +// app/api/test-n8n-config/route.ts +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.json({ + hasN8NApiKey: !!process.env.N8N_API_KEY, + n8nApiKeyLength: process.env.N8N_API_KEY?.length || 0, + n8nWebhookUrl: process.env.N8N_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-created', + missionApiUrl: process.env.NEXT_PUBLIC_API_URL || 'https://api.slm-lab.net/api' + }); +} +``` + +**Usage**: `GET /api/test-n8n-config` + +--- + +### Test 2: Tester Webhook N8N + +```bash +curl -X POST https://brain.slm-lab.net/webhook/mission-created \ + -H "Content-Type: application/json" \ + -d '{"test": "data"}' +``` + +**Résultats attendus**: +- ✅ `200/400/500` avec erreur workflow: Webhook actif +- ❌ `404` avec "webhook not registered": Webhook inactif + +--- + +### Test 3: Tester Callback Endpoint + +```bash +curl -X POST https://api.slm-lab.net/api/missions/mission-created \ + -H "Content-Type: application/json" \ + -H "x-api-key: YOUR_N8N_API_KEY" \ + -d '{ + "missionId": "test-mission-id", + "gitRepoUrl": "https://git.example.com/repo", + "leantimeProjectId": "123" + }' +``` + +**Résultats attendus**: +- ✅ `200` avec `success: true`: API key valide +- ❌ `401`: API key invalide +- ❌ `404`: Mission non trouvée (normal si missionId de test) + +--- + +### Test 4: Créer une Mission Complète + +1. Créer une mission via le frontend +2. Vérifier les logs Next.js: + - ✅ "Mission created successfully" + - ✅ "Starting N8N workflow" + - ✅ "N8N workflow result { success: true }" +3. Vérifier les logs N8N (Executions): + - ✅ Workflow exécuté avec succès + - ✅ Node "Save Mission To API" exécuté +4. Vérifier la base de données: + - ✅ Mission a les IDs d'intégration sauvegardés + +--- + +## 💡 Recommandations + +### 1. Amélioration de la Sécurité + +**Problème actuel**: Clé API en clair, comparaison simple + +**Recommandations**: +- [ ] Utiliser un système de tokens avec expiration +- [ ] Implémenter un système de signature HMAC +- [ ] Ajouter un rate limiting sur `/api/missions/mission-created` +- [ ] Logging des tentatives d'accès invalides avec IP + +--- + +### 2. Amélioration de la Robustesse + +**Problème actuel**: Pas de retry automatique si N8N échoue + +**Recommandations**: +- [ ] Implémenter un système de retry avec backoff exponentiel +- [ ] Queue de messages pour les callbacks manqués +- [ ] Webhook de santé pour vérifier que N8N est accessible +- [ ] Timeout configurable pour les appels N8N + +--- + +### 3. Amélioration du Debugging + +**Problème actuel**: Logs dispersés, pas de traçabilité complète + +**Recommandations**: +- [ ] Ajouter un `correlationId` pour tracer une mission de bout en bout +- [ ] Logs structurés avec contexte complet +- [ ] Dashboard de monitoring des intégrations +- [ ] Alertes en cas d'échec répété + +--- + +### 4. Amélioration de la Documentation + +**Recommandations**: +- [ ] Documenter le format exact attendu par N8N +- [ ] Exemples de payloads complets +- [ ] Diagrammes de séquence détaillés +- [ ] Guide de troubleshooting avec cas réels + +--- + +### 5. Tests Automatisés + +**Recommandations**: +- [ ] Tests unitaires pour `N8nService` +- [ ] Tests d'intégration pour les endpoints API +- [ ] Tests E2E avec mock N8N +- [ ] Tests de charge pour vérifier la scalabilité + +--- + +## 📝 Checklist de Vérification Rapide + +### Configuration +- [ ] `N8N_API_KEY` défini et identique partout +- [ ] `NEXT_PUBLIC_API_URL` pointe vers la bonne URL +- [ ] Application redémarrée après modifications + +### N8N Workflow +- [ ] Workflow actif (toggle vert) +- [ ] Webhook path: `mission-created` +- [ ] Node "Save Mission To API" configuré correctement +- [ ] `missionId` inclus dans le callback + +### Code Next.js +- [ ] `missionId` envoyé à N8N lors de la création +- [ ] Validation API key fonctionnelle +- [ ] Mapping des champs correct +- [ ] Gestion d'erreurs appropriée + +### Tests +- [ ] Test de création de mission réussi +- [ ] IDs d'intégration sauvegardés en base +- [ ] Logs sans erreurs critiques + +--- + +## 🔗 Références + +- **Service N8N**: `lib/services/n8n-service.ts` +- **Endpoint création**: `app/api/missions/route.ts` +- **Endpoint callback**: `app/api/missions/mission-created/route.ts` +- **Documentation N8N**: Voir fichiers `N8N_*.md` dans le projet + +--- + +**Document créé le**: $(date) +**Dernière mise à jour**: $(date) +**Version**: 1.0 + diff --git a/AUTHENTICATION_FIXES.md b/AUTHENTICATION_FIXES.md deleted file mode 100644 index c6f8d69b..00000000 --- a/AUTHENTICATION_FIXES.md +++ /dev/null @@ -1,224 +0,0 @@ -# Authentication Flow Fixes - -## Issues Fixed - -### 1. Logout Loop Issue ✅ - -**Problem**: -- User couldn't log out - infinite redirect loop -- Sign-in page auto-triggered Keycloak login even when user was already authenticated -- Keycloak session cookies weren't cleared, causing immediate re-authentication - -**Root Cause**: -- `/signin` page had `useEffect(() => { signIn("keycloak") }, [])` that always triggered login -- No check for existing authentication status -- Keycloak logout endpoint was never called, leaving Keycloak cookies valid - -**Fix Applied**: -1. **Sign-in page** (`app/signin/page.tsx`): - - Added check for existing session before triggering login - - If user is already authenticated, redirect to home - - Only trigger Keycloak login if status is "unauthenticated" - -2. **Sign-out handler** (`components/auth/signout-handler.tsx`): - - Now properly calls Keycloak logout endpoint - - Uses ID token for proper logout - - Clears both NextAuth and Keycloak cookies - -3. **Main navigation logout** (`components/main-nav.tsx`): - - Fixed to use `idToken` instead of `accessToken` for Keycloak logout - - Proper logout flow with Keycloak endpoint - ---- - -### 2. Iframe Applications Logging Out ✅ - -**Problem**: -- Iframe applications were logging out even when user was still authenticated in dashboard -- Desynchronization between NextAuth session and Keycloak session - -**Root Cause**: -- Sign-out only cleared NextAuth cookies -- Keycloak session cookies remained valid but could expire independently -- Iframe apps rely on Keycloak cookies for SSO -- When Keycloak cookies expired/invalidated, iframes logged out but dashboard stayed logged in - -**Fix Applied**: -1. **ID Token Storage** (`app/api/auth/options.ts`): - - Now stores `idToken` from Keycloak in JWT - - Exposes `idToken` in session object - - Preserves ID token during token refresh - -2. **Proper Keycloak Logout**: - - Sign-out now calls Keycloak logout endpoint with `id_token_hint` - - This properly invalidates Keycloak session and clears Keycloak cookies - - Ensures synchronization between dashboard and iframe apps - -3. **Type Definitions** (`types/next-auth.d.ts`): - - Added `idToken` to Session and JWT interfaces - - Type-safe access to ID token - ---- - -## Changes Made - -### Files Modified - -1. **`app/api/auth/options.ts`** - - Added `idToken` to JWT interface - - Store `account.id_token` in JWT during initial authentication - - Expose `idToken` in session callback - - Preserve `idToken` during token refresh - -2. **`app/signin/page.tsx`** - - Added session status check - - Prevent auto-login if already authenticated - - Redirect authenticated users to home - -3. **`components/auth/signout-handler.tsx`** - - Call Keycloak logout endpoint with ID token - - Proper logout flow that clears both NextAuth and Keycloak sessions - -4. **`components/main-nav.tsx`** - - Fixed logout button to use `idToken` instead of `accessToken` - - Proper Keycloak logout flow - -5. **`types/next-auth.d.ts`** - - Added `idToken?: string` to Session interface - - Added `idToken?: string` to JWT interface (both modules) - ---- - -## How It Works Now - -### Sign-In Flow (Fixed) - -``` -1. User navigates to /signin -2. Check session status: - - If authenticated → Redirect to / - - If unauthenticated → Trigger Keycloak login -3. After Keycloak authentication: - - Store tokens (access, refresh, ID token) - - Initialize storage - - Redirect to dashboard -``` - -### Sign-Out Flow (Fixed) - -``` -1. User clicks logout -2. Sign out from NextAuth (clears NextAuth cookies) -3. Call Keycloak logout endpoint: - - URL: ${KEYCLOAK_ISSUER}/protocol/openid-connect/logout - - Parameters: - * post_logout_redirect_uri: /signin - * id_token_hint: -4. Keycloak clears its session and cookies -5. Redirect to /signin (no auto-login loop) -``` - -### Iframe SSO (Fixed) - -``` -1. User authenticates in dashboard -2. Keycloak sets session cookies -3. Iframe apps read Keycloak cookies -4. When user logs out: - - Keycloak logout endpoint is called - - Keycloak cookies are cleared - - Iframe apps lose access (synchronized logout) -``` - ---- - -## Environment Variables Required - -Ensure these are set: - -```bash -# Required for logout -NEXT_PUBLIC_KEYCLOAK_ISSUER=https://keycloak.example.com/realms/neah - -# Already required for authentication -KEYCLOAK_CLIENT_ID=neah-dashboard -KEYCLOAK_CLIENT_SECRET= -KEYCLOAK_ISSUER=https://keycloak.example.com/realms/neah -NEXTAUTH_URL=https://dashboard.example.com -NEXTAUTH_SECRET= -``` - -**Important**: `NEXT_PUBLIC_KEYCLOAK_ISSUER` must be set for client-side logout to work. - ---- - -## Testing Checklist - -### Logout Flow -- [ ] Click logout button -- [ ] Should redirect to Keycloak logout -- [ ] Should redirect back to /signin -- [ ] Should NOT auto-login (no loop) -- [ ] Should be able to manually log in again - -### Sign-In Flow -- [ ] Navigate to /signin when not authenticated -- [ ] Should trigger Keycloak login -- [ ] Navigate to /signin when already authenticated -- [ ] Should redirect to / (no auto-login trigger) - -### Iframe SSO -- [ ] Log in to dashboard -- [ ] Open iframe application -- [ ] Should be automatically authenticated -- [ ] Log out from dashboard -- [ ] Iframe application should also lose authentication -- [ ] Refresh iframe - should require login - ---- - -## Additional Notes - -### ID Token vs Access Token - -- **Access Token**: Used for API calls to Keycloak-protected resources -- **ID Token**: Used for user identification and logout -- **Refresh Token**: Used to get new access tokens - -The ID token is required for proper Keycloak logout. It tells Keycloak which session to invalidate. - -### Cookie Synchronization - -The fix ensures that: -1. NextAuth cookies are cleared (dashboard logout) -2. Keycloak cookies are cleared (via logout endpoint) -3. Both happen in sequence, maintaining synchronization - -### Token Refresh - -During token refresh, the ID token is preserved (Keycloak doesn't issue new ID tokens on refresh). This ensures logout continues to work even after token refreshes. - ---- - -## Troubleshooting - -### If logout still loops: - -1. Check browser console for errors -2. Verify `NEXT_PUBLIC_KEYCLOAK_ISSUER` is set correctly -3. Check that Keycloak logout endpoint is accessible -4. Verify ID token is present in session: `console.log(session?.idToken)` - -### If iframes still log out independently: - -1. Check Keycloak cookie domain configuration -2. Verify iframe apps are configured to use same Keycloak realm -3. Check browser cookie settings (third-party cookies may be blocked) -4. Verify Keycloak session timeout settings - ---- - -**Date**: 2024 -**Status**: ✅ Fixed -**Version**: 1.0 - diff --git a/AUTHENTICATION_FLOW_AUDIT.md b/AUTHENTICATION_FLOW_AUDIT.md deleted file mode 100644 index c5a4be4e..00000000 --- a/AUTHENTICATION_FLOW_AUDIT.md +++ /dev/null @@ -1,988 +0,0 @@ -# Authentication Flow Audit - NextAuth with Keycloak & SSO for Iframe Applications - -## Executive Summary - -This document provides a comprehensive audit of the authentication architecture in the Neah dashboard application. The system uses **NextAuth.js v4** with **Keycloak** as the OAuth provider, implementing JWT-based sessions and supporting Single Sign-On (SSO) for multiple iframe-embedded applications via cookie-based authentication. - ---- - -## Architecture Overview - -### Components -1. **NextAuth.js** - Authentication framework -2. **Keycloak** - Identity Provider (IdP) via OAuth 2.0/OpenID Connect -3. **JWT Strategy** - Session management (no database sessions) -4. **Iframe Applications** - Multiple embedded applications using SSO via cookies -5. **Keycloak Admin Client** - Server-side user management - ---- - -## 1. Authentication Entry Points - -### 1.1 Sign-In Page (`/app/signin/page.tsx`) - -**Location**: `app/signin/page.tsx` - -**Flow**: -```typescript -1. User navigates to /signin -2. Component automatically triggers: signIn("keycloak", { callbackUrl: "/" }) -3. Redirects to Keycloak authorization endpoint -4. After Keycloak authentication, initializes storage via /api/storage/init -5. Redirects to home page -``` - -**Key Methods**: -- `signIn("keycloak")` - NextAuth client-side method -- Automatic redirect to Keycloak OAuth flow -- Storage initialization after successful authentication - -**Dependencies**: -- `next-auth/react` - Client-side NextAuth hooks -- Storage API endpoint for user space initialization - ---- - -## 2. NextAuth Configuration - -### 2.1 Route Handler (`/app/api/auth/[...nextauth]/route.ts`) - -**Location**: `app/api/auth/[...nextauth]/route.ts` - -**Purpose**: NextAuth API route handler for all authentication endpoints - -**Endpoints Handled**: -- `GET/POST /api/auth/signin` - Sign in -- `GET/POST /api/auth/signout` - Sign out -- `GET /api/auth/session` - Get current session -- `GET /api/auth/csrf` - CSRF token -- `GET /api/auth/providers` - Available providers -- `GET /api/auth/callback/keycloak` - OAuth callback - -**Implementation**: -```typescript -import NextAuth from "next-auth"; -import { authOptions } from "../options"; - -const handler = NextAuth(authOptions); -export { handler as GET, handler as POST }; -``` - ---- - -### 2.2 Auth Options Configuration (`/app/api/auth/options.ts`) - -**Location**: `app/api/auth/options.ts` - -**This is the core authentication configuration file.** - -#### 2.2.1 Keycloak Provider Setup - -```typescript -KeycloakProvider({ - clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"), - clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"), - issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"), - authorization: { - params: { - scope: "openid profile email roles" // Requested OAuth scopes - } - }, - profile(profile) { /* Profile transformation */ } -}) -``` - -**Environment Variables Required**: -- `KEYCLOAK_CLIENT_ID` - OAuth client identifier -- `KEYCLOAK_CLIENT_SECRET` - OAuth client secret -- `KEYCLOAK_ISSUER` - Keycloak realm issuer URL (e.g., `https://keycloak.example.com/realms/neah`) - -**OAuth Scopes Requested**: -- `openid` - OpenID Connect core -- `profile` - User profile information -- `email` - User email address -- `roles` - User roles from Keycloak realm - -#### 2.2.2 Profile Callback - -**Location**: Lines 109-137 in `options.ts` - -**Purpose**: Transforms Keycloak user profile into NextAuth user object - -**Process**: -1. Receives Keycloak profile with `realm_access.roles` -2. Extracts roles from `realm_access.roles` array -3. Cleans roles by: - - Removing `ROLE_` prefix (if present) - - Converting to lowercase -4. Maps Keycloak profile fields to NextAuth user: - - `sub` → `id` - - `name` or `preferred_username` → `name` - - `email` → `email` - - `given_name` → `first_name` - - `family_name` → `last_name` - - `preferred_username` → `username` - - Cleaned roles → `role[]` - -**Code Flow**: -```typescript -profile(profile) { - const roles = profile.realm_access?.roles || []; - const cleanRoles = roles.map((role: string) => - role.replace(/^ROLE_/, '').toLowerCase() - ); - - return { - id: profile.sub, - name: profile.name ?? profile.preferred_username, - email: profile.email, - first_name: profile.given_name ?? '', - last_name: profile.family_name ?? '', - username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '', - role: cleanRoles, - } -} -``` - -#### 2.2.3 Session Configuration - -**Location**: Lines 140-143 - -```typescript -session: { - strategy: "jwt", // JWT-based sessions (no database) - maxAge: 30 * 24 * 60 * 60, // 30 days -} -``` - -**Characteristics**: -- **Strategy**: JWT (stateless, no database lookups) -- **Max Age**: 30 days (2,592,000 seconds) -- **Storage**: Encrypted JWT stored in HTTP-only cookies - -#### 2.2.4 JWT Callback - -**Location**: Lines 145-181 - -**Purpose**: Handles JWT token creation and refresh - -**Flow**: - -**Initial Authentication (account & profile present)**: -```typescript -if (account && profile) { - 1. Extract roles from Keycloak profile - 2. Clean roles (remove ROLE_ prefix, lowercase) - 3. Store in JWT token: - - accessToken: account.access_token (Keycloak access token) - - refreshToken: account.refresh_token (Keycloak refresh token) - - accessTokenExpires: account.expires_at (expiration timestamp) - - sub: Keycloak user ID - - role: cleaned roles array - - username, first_name, last_name: from profile -} -``` - -**Subsequent Requests (token refresh check)**: -```typescript -else if (token.accessToken) { - 1. Decode JWT to extract roles (if not already in token) - 2. Check if token is expired: - - If expired: Call refreshAccessToken() - - If valid: Return existing token -} -``` - -**Token Expiration Check**: -```typescript -if (Date.now() < (token.accessTokenExpires as number) * 1000) { - return token; // Token still valid -} -return refreshAccessToken(token); // Token expired, refresh -``` - -**Note**: There's a **BUG** in line 176 - it multiplies `accessTokenExpires` by 1000, but `expires_at` from Keycloak is already in seconds since epoch. This should be checked. - -#### 2.2.5 Token Refresh Function - -**Location**: Lines 64-96 - -**Purpose**: Refreshes expired Keycloak access tokens - -**Implementation**: -```typescript -async function refreshAccessToken(token: JWT) { - 1. POST to Keycloak token endpoint: - - URL: ${KEYCLOAK_ISSUER}/protocol/openid-connect/token - - Method: POST - - Body: - * client_id: KEYCLOAK_CLIENT_ID - * client_secret: KEYCLOAK_CLIENT_SECRET - * grant_type: refresh_token - * refresh_token: token.refreshToken - - 2. On Success: - - Update accessToken - - Update refreshToken (if new one provided) - - Update accessTokenExpires: Date.now() + expires_in * 1000 - - 3. On Error: - - Set token.error = "RefreshAccessTokenError" - - Return token with error flag -} -``` - -**Error Handling**: Sets `token.error` flag which is checked in session callback - -#### 2.2.6 Session Callback - -**Location**: Lines 182-202 - -**Purpose**: Transforms JWT token into session object for client-side use - -**Flow**: -```typescript -async session({ session, token }) { - 1. Check for refresh errors: - if (token.error) throw new Error(token.error) - - 2. Build session.user object: - - id: token.sub (Keycloak user ID) - - email: token.email - - name: token.name - - image: null - - username: token.username - - first_name: token.first_name - - last_name: token.last_name - - role: token.role (array) - - nextcloudInitialized: false (default) - - 3. Add accessToken to session: - session.accessToken = token.accessToken - - 4. Return session -} -``` - -**Important**: The `accessToken` (Keycloak OAuth token) is exposed in the session object, making it available client-side via `useSession()` hook. - -#### 2.2.7 Custom Pages - -**Location**: Lines 204-207 - -```typescript -pages: { - signIn: '/signin', - error: '/signin', -} -``` - -**Custom Routes**: -- Sign-in page: `/signin` (instead of default `/api/auth/signin`) -- Error page: `/signin` (redirects to sign-in on errors) - ---- - -## 3. Authentication Flow Step-by-Step - -### 3.1 Initial Sign-In Flow - -``` -┌─────────────┐ -│ Browser │ -└──────┬──────┘ - │ - │ 1. GET /signin - ▼ -┌─────────────────────┐ -│ /app/signin/page.tsx │ -│ - Auto-triggers │ -│ signIn("keycloak") │ -└──────┬──────────────┘ - │ - │ 2. Redirect to NextAuth - ▼ -┌──────────────────────────────┐ -│ /api/auth/signin/keycloak │ -│ - Generates OAuth state │ -│ - Redirects to Keycloak │ -└──────┬───────────────────────┘ - │ - │ 3. GET /realms/{realm}/protocol/openid-connect/auth - │ ?client_id=... - │ &redirect_uri=... - │ &response_type=code - │ &scope=openid profile email roles - │ &state=... - ▼ -┌─────────────────────┐ -│ Keycloak Server │ -│ - Login page │ -│ - User credentials │ -└──────┬──────────────┘ - │ - │ 4. User authenticates - │ - │ 5. POST /realms/{realm}/protocol/openid-connect/token - │ (Authorization code exchange) - │ - │ 6. Keycloak returns: - │ - access_token - │ - refresh_token - │ - id_token - │ - expires_in - ▼ -┌──────────────────────────────┐ -│ /api/auth/callback/keycloak │ -│ - Receives authorization code│ -│ - Exchanges for tokens │ -│ - Fetches user profile │ -└──────┬───────────────────────┘ - │ - │ 7. JWT Callback - │ - Stores tokens in JWT - │ - Extracts user info - │ - Cleans roles - │ - │ 8. Session Callback - │ - Builds session object - │ - │ 9. Sets NextAuth cookies: - │ - next-auth.session-token (encrypted JWT) - │ - next-auth.csrf-token - ▼ -┌─────────────────────┐ -│ Browser (Client) │ -│ - Cookies set │ -│ - Redirect to / │ -└──────┬──────────────┘ - │ - │ 10. GET / (home page) - │ - getServerSession() validates JWT - │ - Session available - ▼ -┌─────────────────────┐ -│ Dashboard Loaded │ -└─────────────────────┘ -``` - -### 3.2 Subsequent Request Flow (Authenticated) - -``` -┌─────────────┐ -│ Browser │ -└──────┬──────┘ - │ - │ 1. GET /any-page - │ Cookie: next-auth.session-token=... - ▼ -┌──────────────────────────────┐ -│ Next.js Server │ -│ getServerSession(authOptions)│ -└──────┬───────────────────────┘ - │ - │ 2. Decrypt JWT from cookie - │ - │ 3. Check token expiration - │ - │ 4a. If valid: - │ - Extract user info - │ - Return session - │ - │ 4b. If expired: - │ - Call refreshAccessToken() - │ - POST to Keycloak /token - │ - Update JWT with new tokens - │ - Return session - ▼ -┌─────────────────────┐ -│ Page Component │ -│ - session available│ -└─────────────────────┘ -``` - -### 3.3 Token Refresh Flow - -``` -┌─────────────────────┐ -│ JWT Callback │ -│ (Token expired) │ -└──────┬──────────────┘ - │ - │ 1. Call refreshAccessToken() - │ - │ 2. POST ${KEYCLOAK_ISSUER}/protocol/openid-connect/token - │ Body: - │ - client_id - │ - client_secret - │ - grant_type: refresh_token - │ - refresh_token: - ▼ -┌─────────────────────┐ -│ Keycloak Server │ -│ - Validates refresh│ -│ token │ -│ - Issues new tokens│ -└──────┬──────────────┘ - │ - │ 3. Returns: - │ - access_token (new) - │ - refresh_token (new, optional) - │ - expires_in - ▼ -┌─────────────────────┐ -│ Update JWT Token │ -│ - New accessToken │ -│ - New refreshToken │ -│ - New expires time │ -└──────┬──────────────┘ - │ - │ 4. Return updated token - │ - │ 5. Session callback builds session - ▼ -┌─────────────────────┐ -│ Session Available │ -└─────────────────────┘ -``` - ---- - -## 4. Iframe SSO Architecture - -### 4.1 Overview - -The dashboard embeds multiple applications in iframes. These applications rely on **cookie-based SSO** to authenticate users automatically using the Keycloak session established in the parent dashboard. - -### 4.2 Iframe Application Pages - -**Pattern**: All iframe pages follow the same structure: - -```typescript -// Example: app/parole/page.tsx -export default async function Page() { - const session = await getServerSession(authOptions); - - if (!session) { - redirect("/signin"); - } - - return ( - - ); -} -``` - -**Iframe Applications Identified**: -1. **Parole** (`/parole`) - `NEXT_PUBLIC_IFRAME_PAROLE_URL` -2. **Agilite** (`/agilite`) - `NEXT_PUBLIC_IFRAME_AGILITY_URL` -3. **Alma** (`/alma`) - `NEXT_PUBLIC_IFRAME_AI_ASSISTANT_URL` -4. **Vision** (`/vision`) - `NEXT_PUBLIC_IFRAME_CONFERENCE_URL` -5. **The Message** (`/the-message`) - `NEXT_PUBLIC_IFRAME_THEMESSAGE_URL` -6. **WP Admin** (`/wp-admin`) - `NEXT_PUBLIC_IFRAME_MISSIONVIEW_URL` -7. **Mediation** (`/mediation`) - `NEXT_PUBLIC_IFRAME_MEDIATIONS_URL` -8. **Apprendre** (`/apprendre`) - `NEXT_PUBLIC_IFRAME_LEARN_URL` -9. **Gite** (`/gite`) - `NEXT_PUBLIC_IFRAME_GITE_URL` -10. **Artlab** (`/artlab`) - `NEXT_PUBLIC_IFRAME_ARTLAB_URL` -11. **Calcul** (`/calcul`) - `NEXT_PUBLIC_IFRAME_CALCULATION_URL` -12. **Chapitre** (`/chapitre`) - `NEXT_PUBLIC_IFRAME_CHAPTER_URL` -13. **Dossiers** (`/dossiers`) - `NEXT_PUBLIC_IFRAME_DRIVE_URL` -14. **CRM** (`/crm`) - `NEXT_PUBLIC_IFRAME_MEDIATIONS_URL` -15. **Livres** (`/livres`) - `NEXT_PUBLIC_IFRAME_LIVRE_URL` -16. **Showcase** (`/showcase`) - `NEXT_PUBLIC_IFRAME_SHOWCASE_URL` -17. **Radio** (`/radio`) - `NEXT_PUBLIC_IFRAME_RADIO_URL` -18. **Press** (`/press`) - `NEXT_PUBLIC_IFRAME_SHOWCASE_URL` -19. **Observatory** - `NEXT_PUBLIC_IFRAME_OBSERVATORY_URL` -20. **Time Tracker** - `NEXT_PUBLIC_IFRAME_TIMETRACKER_URL` -21. **Missions Board** - `NEXT_PUBLIC_IFRAME_MISSIONSBOARD_URL` -22. **Carnet** - `NEXT_PUBLIC_IFRAME_CARNET_URL` - -### 4.3 SSO Cookie Mechanism - -**How It Works**: - -1. **Parent Dashboard Authentication**: - - User authenticates via Keycloak in the dashboard - - Keycloak sets authentication cookies (domain: Keycloak domain) - - NextAuth sets session cookies (domain: dashboard domain) - -2. **Iframe Cookie Sharing**: - - When iframe loads, browser sends cookies for the iframe's domain - - If iframe application is on **same domain** or **subdomain** of Keycloak: - - Keycloak cookies are automatically sent - - Application can read Keycloak session cookies - - SSO works automatically - -3. **Cross-Domain Considerations**: - - If iframe apps are on different domains, they need: - - Same Keycloak realm configuration - - Proper CORS settings - - Cookie domain configuration in Keycloak - - `SameSite=None; Secure` cookie attributes for cross-site - -### 4.4 ResponsiveIframe Component - -**Location**: `app/components/responsive-iframe.tsx` - -**Features**: -- Auto-resizing based on viewport -- Hash synchronization (URL fragments) -- Full-screen support - -**Important**: This component does **NOT** handle authentication - it's purely presentational. SSO relies on browser cookie behavior. - ---- - -## 5. Sign-Out Flow - -### 5.1 Sign-Out Page - -**Location**: `app/signout/page.tsx` - -**Implementation**: -```typescript -export default function SignOut() { - return ( -
- -

Déconnexion en cours...

-
- ); -} -``` - -### 5.2 Sign-Out Handler - -**Location**: `components/auth/signout-handler.tsx` - -**Flow**: -```typescript -1. clearAuthCookies() - Clears NextAuth cookies client-side -2. signOut({ callbackUrl: "/signin", redirect: true }) - - Calls NextAuth signout endpoint - - Invalidates session - - Redirects to /signin -``` - -### 5.3 Cookie Clearing - -**Location**: `lib/session.ts` - `clearAuthCookies()` - -**Implementation**: -```typescript -export function clearAuthCookies() { - const cookies = document.cookie.split(';'); - for (const cookie of cookies) { - const [name] = cookie.split('='); - if (name.trim().startsWith('next-auth.') || - name.trim().startsWith('__Secure-next-auth.') || - name.trim().startsWith('__Host-next-auth.')) { - document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; - } - } -} -``` - -**Note**: This only clears NextAuth cookies. Keycloak cookies remain unless: -- User manually logs out of Keycloak -- Keycloak session expires -- Application calls Keycloak logout endpoint - -### 5.4 Service Token Invalidation - -**Location**: `lib/session.ts` - `invalidateServiceTokens()` - -**Purpose**: Logs out from integrated services (RocketChat, Leantime, etc.) - -**Services Handled**: -- RocketChat: `/api/v1/logout` -- Leantime: JSON-RPC logout method - -**Note**: This function exists but may not be called during standard sign-out flow. - ---- - -## 6. Server-Side Session Access - -### 6.1 getServerSession() - -**Usage Pattern** (seen in all iframe pages): -```typescript -import { getServerSession } from "next-auth/next"; -import { authOptions } from "@/app/api/auth/options"; - -const session = await getServerSession(authOptions); -``` - -**How It Works**: -1. Reads `next-auth.session-token` cookie from request -2. Decrypts JWT using `NEXTAUTH_SECRET` -3. Validates token signature and expiration -4. If expired, triggers refresh (via JWT callback) -5. Returns session object - -**Location**: Used in: -- All iframe page components -- Root layout (`app/layout.tsx`) -- Any server component needing authentication - -### 6.2 Client-Side Session Access - -**Usage Pattern**: -```typescript -import { useSession } from "next-auth/react"; - -const { data: session, status } = useSession(); -``` - -**How It Works**: -1. `useSession()` hook calls `/api/auth/session` -2. Server decrypts JWT and returns session -3. Client receives session object -4. Automatically refetches when token refreshes - -**Location**: Used in: -- `app/signin/page.tsx` -- `components/auth/auth-check.tsx` -- Any client component needing authentication - ---- - -## 7. Keycloak Admin Client - -### 7.1 Purpose - -**Location**: `lib/keycloak.ts` - -The Keycloak Admin Client is used for **server-side user management**, not for user authentication. It's a separate administrative interface. - -### 7.2 Authentication Methods - -**Two Methods Supported**: - -1. **Client Credentials** (Preferred): - ```typescript - grant_type: 'client_credentials' - client_id: KEYCLOAK_CLIENT_ID - client_secret: KEYCLOAK_CLIENT_SECRET - ``` - -2. **Password Grant** (Fallback): - ```typescript - grant_type: 'password' - client_id: KEYCLOAK_CLIENT_ID - username: KEYCLOAK_ADMIN_USERNAME - password: KEYCLOAK_ADMIN_PASSWORD - ``` - -### 7.3 Caching - -**Token Caching**: 5 minutes -- Validates cached token before reuse -- Creates new client if token invalid/expired - -### 7.4 Functions - -- `getKeycloakAdminClient()` - Get authenticated admin client -- `getUserById(userId)` - Get user by Keycloak ID -- `getUserByEmail(email)` - Get user by email -- `getAllRoles()` - Get all realm roles -- `getUserRoles(userId)` - Get user's role mappings - ---- - -## 8. Security Considerations - -### 8.1 Cookie Security - -**NextAuth Cookie Configuration** (implicit): -- **HttpOnly**: Yes (prevents XSS access) -- **Secure**: Yes (if `NEXTAUTH_URL` starts with `https://`) -- **SameSite**: Lax (default) -- **Path**: `/` -- **Domain**: Dashboard domain - -**Keycloak Cookie Configuration** (Keycloak-controlled): -- Set by Keycloak server -- Typically `SameSite=Lax` or `SameSite=None` (for cross-site) -- Domain: Keycloak domain or configured domain - -### 8.2 Token Storage - -- **Access Token**: Stored in encrypted JWT (server-side only accessible) -- **Refresh Token**: Stored in encrypted JWT -- **Session Token**: Encrypted JWT in HTTP-only cookie - -**Client-Side Access**: -- `session.accessToken` is exposed to client via `useSession()` -- This is the Keycloak OAuth access token -- Can be used for API calls to Keycloak-protected resources - -### 8.3 CORS & CSP - -**Content Security Policy** (`next.config.mjs`): -```typescript -'Content-Security-Policy': "frame-ancestors 'self' https://espace.slm-lab.net https://connect.slm-lab.net" -``` - -**Allows framing from**: -- Same origin (`'self'`) -- `https://espace.slm-lab.net` -- `https://connect.slm-lab.net` - -### 8.4 Role-Based Access Control - -**Role Extraction**: -- Roles come from Keycloak `realm_access.roles` -- Cleaned: `ROLE_` prefix removed, lowercased -- Stored in session: `session.user.role[]` - -**Usage**: Roles are available but not actively enforced in the codebase audit. Applications should implement RBAC checks. - ---- - -## 9. Environment Variables - -### 9.1 Required for Authentication - -```bash -# Keycloak OAuth Configuration -KEYCLOAK_CLIENT_ID=neah-dashboard -KEYCLOAK_CLIENT_SECRET= -KEYCLOAK_ISSUER=https://keycloak.example.com/realms/neah -KEYCLOAK_REALM=neah - -# NextAuth Configuration -NEXTAUTH_URL=https://dashboard.example.com -NEXTAUTH_SECRET= - -# Keycloak Admin (optional, for user management) -KEYCLOAK_ADMIN_USERNAME=admin -KEYCLOAK_ADMIN_PASSWORD= -KEYCLOAK_BASE_URL=https://keycloak.example.com -``` - -### 9.2 Iframe Application URLs - -All iframe applications require `NEXT_PUBLIC_IFRAME_*` environment variables: -- `NEXT_PUBLIC_IFRAME_PAROLE_URL` -- `NEXT_PUBLIC_IFRAME_AGILITY_URL` -- `NEXT_PUBLIC_IFRAME_AI_ASSISTANT_URL` -- `NEXT_PUBLIC_IFRAME_CONFERENCE_URL` -- ... (see section 4.2 for complete list) - ---- - -## 10. Potential Issues & Recommendations - -### 10.1 Token Expiration Bug - -**Location**: `app/api/auth/options.ts:176` - -```typescript -if (Date.now() < (token.accessTokenExpires as number) * 1000) { -``` - -**Issue**: `accessTokenExpires` from Keycloak `account.expires_at` is already in seconds since epoch. Multiplying by 1000 assumes it's in milliseconds, which may cause incorrect expiration checks. - -**Recommendation**: Verify Keycloak's `expires_at` format. If it's in seconds, remove the `* 1000`. If it's in milliseconds, keep it. - -### 10.2 Cookie SameSite for Cross-Domain Iframes - -**Issue**: If iframe applications are on different domains, Keycloak cookies may not be sent due to `SameSite` restrictions. - -**Recommendation**: -- Configure Keycloak cookies with `SameSite=None; Secure` -- Ensure all domains use HTTPS -- Consider using a shared parent domain for cookies - -### 10.3 Access Token Exposure - -**Issue**: `session.accessToken` (Keycloak OAuth token) is exposed client-side. - -**Recommendation**: -- Only expose if needed for client-side API calls -- Consider using proxy endpoints instead -- Implement token rotation if exposed - -### 10.4 No Explicit Cookie Configuration - -**Issue**: NextAuth cookie settings are implicit (defaults). - -**Recommendation**: Explicitly configure cookies in `authOptions`: -```typescript -cookies: { - sessionToken: { - name: `next-auth.session-token`, - options: { - httpOnly: true, - sameSite: 'lax', - path: '/', - secure: process.env.NEXTAUTH_URL?.startsWith('https://') ?? false, - } - } -} -``` - -### 10.5 Storage Initialization - -**Issue**: Storage initialization happens client-side after authentication, which may cause race conditions. - -**Recommendation**: Move storage initialization to server-side or use a more robust initialization pattern. - -### 10.6 Service Token Invalidation Not Called - -**Issue**: `invalidateServiceTokens()` exists but may not be called during sign-out. - -**Recommendation**: Integrate service token invalidation into the sign-out flow. - ---- - -## 11. Flow Diagrams - -### 11.1 Complete Authentication Flow - -``` -User → /signin - → signIn("keycloak") - → /api/auth/signin/keycloak - → Keycloak Authorization Endpoint - → User Login (Keycloak) - → Keycloak Token Endpoint - → /api/auth/callback/keycloak - → JWT Callback (store tokens) - → Session Callback (build session) - → Set Cookies - → Redirect to / - → Storage Init - → Dashboard Loaded -``` - -### 11.2 Iframe SSO Flow - -``` -Dashboard (authenticated) - → User clicks iframe app link - → Server checks session (getServerSession) - → If authenticated: Load iframe - → Browser sends cookies to iframe domain - → Iframe app reads Keycloak cookies - → Iframe app validates session - → Iframe app loads authenticated -``` - -### 11.3 Token Refresh Flow - -``` -Request with expired token - → getServerSession() - → Decrypt JWT - → Check expiration - → If expired: JWT Callback - → refreshAccessToken() - → POST to Keycloak /token - → Get new tokens - → Update JWT - → Return session -``` - ---- - -## 12. File Reference Map - -### Core Authentication Files - -| File | Purpose | -|------|---------| -| `app/api/auth/[...nextauth]/route.ts` | NextAuth route handler | -| `app/api/auth/options.ts` | **Main auth configuration** | -| `app/signin/page.tsx` | Sign-in page | -| `app/signout/page.tsx` | Sign-out page | -| `components/auth/signout-handler.tsx` | Sign-out logic | -| `components/auth/auth-check.tsx` | Client-side auth guard | -| `lib/keycloak.ts` | Keycloak admin client | -| `lib/session.ts` | Session utilities | -| `types/next-auth.d.ts` | TypeScript definitions | - -### Iframe Application Files - -All in `app/*/page.tsx`: -- `app/parole/page.tsx` -- `app/agilite/page.tsx` -- `app/alma/page.tsx` -- `app/vision/page.tsx` -- ... (see section 4.2) - -### Supporting Files - -| File | Purpose | -|------|---------| -| `app/components/responsive-iframe.tsx` | Iframe component | -| `app/layout.tsx` | Root layout (session check) | -| `components/providers.tsx` | SessionProvider wrapper | -| `components/layout/layout-wrapper.tsx` | Layout wrapper with auth | - ---- - -## 13. Testing Checklist - -### Authentication Flow -- [ ] Sign-in redirects to Keycloak -- [ ] Keycloak login works -- [ ] Callback receives tokens -- [ ] Session is created -- [ ] Cookies are set -- [ ] User redirected to dashboard -- [ ] Storage initializes - -### Session Management -- [ ] Session persists across page reloads -- [ ] Token refresh works when expired -- [ ] Session expires after 30 days -- [ ] Invalid tokens are rejected - -### Sign-Out -- [ ] Sign-out clears NextAuth cookies -- [ ] User redirected to sign-in -- [ ] Session invalidated - -### Iframe SSO -- [ ] Iframe apps receive Keycloak cookies -- [ ] Iframe apps authenticate automatically -- [ ] Cross-domain cookies work (if applicable) -- [ ] Unauthenticated users redirected - -### Security -- [ ] HttpOnly cookies enforced -- [ ] Secure cookies on HTTPS -- [ ] CSRF protection active -- [ ] Token encryption working - ---- - -## 14. Conclusion - -The authentication architecture uses a standard NextAuth + Keycloak OAuth 2.0 flow with JWT-based sessions. The system supports SSO for iframe applications via cookie sharing, assuming proper domain configuration. - -**Key Strengths**: -- Standard OAuth 2.0/OpenID Connect implementation -- Stateless JWT sessions (scalable) -- Automatic token refresh -- Role-based user information - -**Areas for Improvement**: -- Explicit cookie configuration -- Token expiration bug fix -- Service token invalidation integration -- Cross-domain cookie configuration verification -- Storage initialization robustness - ---- - -**Document Version**: 1.0 -**Last Updated**: 2024 -**Audited By**: AI Assistant -**Next Review**: After implementing recommendations - diff --git a/CAPROVER_NGINX_FIX.md b/CAPROVER_NGINX_FIX.md deleted file mode 100644 index 76d2b8e4..00000000 --- a/CAPROVER_NGINX_FIX.md +++ /dev/null @@ -1,163 +0,0 @@ -# Fix Nginx CapRover - Erreur "upstream sent too big header" - -## 🔍 Problème - -Erreur 502 avec message Nginx : -``` -upstream sent too big header while reading response header from upstream -``` - -**Cause** : Le cookie de session NextAuth (JWT avec tokens Keycloak) dépasse 4KB, la limite par défaut de Nginx. - -## ✅ Solution : Modifier la configuration CapRover - -### Option 1 : Via CapRover Dashboard (RECOMMANDÉ) - -1. **Aller dans CapRover Dashboard** -2. **Sélectionner votre app** (hub.slm-lab.net) -3. **Aller dans "HTTP Settings"** -4. **Cliquer sur "Edit Nginx Configuration"** (si disponible) -5. **OU aller dans "App Configs" → "nginx"** - -### Option 2 : Modifier le template Nginx directement - -Si vous avez accès au serveur CapRover, modifier le template dans : -- `/captain/templates/nginx.conf` (template principal) -- OU créer un override dans votre app - -## 📝 Configuration à Ajouter - -**Dans le bloc `location /`**, ajouter ces directives **AVANT** `proxy_pass` : - -```nginx -location / { - # ============================================ - # FIX: Augmenter la limite des headers pour NextAuth - # ============================================ - proxy_buffer_size 16k; - proxy_buffers 8 16k; - proxy_busy_buffers_size 32k; - large_client_header_buffers 4 32k; - - # Timeouts (pour éviter les timeouts) - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - - # Configuration proxy existante - proxy_pass $upstream; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # WebSocket support (si activé) - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_http_version 1.1; -} -``` - -## 🔧 Configuration Complète Modifiée - -Voici le bloc `location /` complet avec les corrections : - -```nginx -location / { - # FIX: Headers trop grands pour NextAuth - proxy_buffer_size 16k; - proxy_buffers 8 16k; - proxy_busy_buffers_size 32k; - large_client_header_buffers 4 32k; - - # Timeouts - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - - # Proxy configuration - proxy_pass $upstream; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # WebSocket (si activé) - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_http_version 1.1; -} -``` - -## 🎯 Méthode via CapRover Dashboard - -### Si CapRover permet l'édition Nginx : - -1. **Dashboard CapRover** → Votre app -2. **"App Configs"** → **"nginx"** -3. **Ajouter dans "Custom Nginx Configuration"** : - -```nginx -location / { - proxy_buffer_size 16k; - proxy_buffers 8 16k; - proxy_busy_buffers_size 32k; - large_client_header_buffers 4 32k; -} -``` - -4. **Sauvegarder** → CapRover regénère la config - -### Si CapRover ne permet pas l'édition : - -**Option A** : Modifier le template CapRover (avancé) -- Accéder au serveur CapRover -- Modifier `/captain/templates/nginx.conf` -- Redémarrer CapRover - -**Option B** : Créer un fichier de configuration personnalisé -- Créer un fichier dans votre app -- L'inclure dans la config Nginx - -## 🔄 Après Modification - -1. **Vérifier la config Nginx** : -```bash -sudo nginx -t -``` - -2. **Recharger Nginx** : -```bash -sudo systemctl reload nginx -# OU si CapRover gère Nginx -docker exec captain-nginx nginx -s reload -``` - -3. **Tester la connexion** : -- Se connecter via Keycloak -- Vérifier que l'erreur 502 ne se produit plus - -## 📊 Explication - -**Avant** : -- Limite par défaut Nginx : 4KB pour les headers -- Cookie NextAuth : ~4-7KB (JWT avec tokens Keycloak) -- Résultat : ❌ Erreur 502 - -**Après** : -- Limite augmentée : 32KB pour les headers -- Cookie NextAuth : ~4-7KB -- Résultat : ✅ Fonctionne - -## ⚠️ Note Importante - -Si vous modifiez le template CapRover directement, **vos modifications seront écrasées** lors d'une mise à jour de CapRover. - -**Recommandation** : Utiliser la méthode "Custom Nginx Configuration" dans CapRover si disponible, ou documenter vos modifications pour les réappliquer après mise à jour. - ---- - -**Document créé le** : $(date) -**Priorité** : HAUTE - Résout l'erreur 502 - - diff --git a/CHANGELOG_LOGIN_IMPROVEMENTS.md b/CHANGELOG_LOGIN_IMPROVEMENTS.md deleted file mode 100644 index 88071ce1..00000000 --- a/CHANGELOG_LOGIN_IMPROVEMENTS.md +++ /dev/null @@ -1,199 +0,0 @@ -# Changelog - Améliorations Flow de Connexion - -## Date : $(date) - -### ✅ Modifications Effectuées - -#### 1. **Suppression de `prompt=login` par défaut** ⭐ -**Fichier** : `app/api/auth/options.ts` -- **Avant** : `prompt: "login"` toujours actif → empêchait SSO naturel -- **Après** : `prompt` supprimé → SSO fonctionne naturellement pour utilisateurs légitimes -- **Impact** : ✅ Meilleure UX - SSO fonctionne pour les utilisateurs légitimes - -#### 2. **Création route API `/api/auth/mark-logout`** ⭐ -**Nouveau fichier** : `app/api/auth/mark-logout/route.ts` -- Crée un cookie HttpOnly `force_login_prompt=true` (5 minutes) -- Utilisé pour marquer qu'un logout a eu lieu -- **Impact** : ✅ Mécanisme robuste pour détecter logout côté serveur - -#### 3. **Modification signout-handler.tsx** ⭐ -**Fichier** : `components/auth/signout-handler.tsx` -- Ajout appel à `/api/auth/mark-logout` avant logout -- **Impact** : ✅ Flag serveur créé pour forcer login après logout - -#### 4. **Modification main-nav.tsx** ⭐ -**Fichier** : `components/main-nav.tsx` -- Ajout appel à `/api/auth/mark-logout` dans le handler logout -- **Impact** : ✅ Logout depuis navigation marque correctement le logout - -#### 5. **Modification layout-wrapper.tsx** ⭐ -**Fichier** : `components/layout/layout-wrapper.tsx` -- Ajout appel à `/api/auth/mark-logout` lors de logout depuis iframe -- **Impact** : ✅ Logout depuis iframe marque correctement le logout - -#### 6. **Simplification signin/page.tsx** ⭐ -**Fichier** : `app/signin/page.tsx` -- **Avant** : Logique complexe avec vérifications multiples (cookies, sessionStorage, URL params) -- **Après** : Logique simplifiée basée sur cookie serveur `force_login_prompt` -- Suppression de la détection complexe de session invalide -- **Impact** : ✅ Code plus simple et maintenable, moins de race conditions - -#### 7. **Configuration explicite des cookies NextAuth** ⭐ -**Fichier** : `app/api/auth/options.ts` -- Ajout configuration explicite pour : - - `sessionToken` - - `callbackUrl` - - `csrfToken` - - `state` -- **Impact** : ✅ Meilleur contrôle et sécurité des cookies - -#### 8. **Amélioration gestion erreur refresh token** ⭐ -**Fichier** : `app/api/auth/options.ts` -- Détection améliorée des différents types d'erreurs : - - `SessionNotActive` (session invalidée) - - `RefreshTokenExpired` (token expiré) -- Suppression explicite des tokens lors d'erreurs -- Clear des erreurs précédentes lors de refresh réussi -- **Impact** : ✅ Meilleure détection et gestion des sessions invalides - ---- - -## 🔄 Comportement Avant/Après - -### Avant les modifications - -**Login première visite** : -- ❌ Toujours demander credentials (même si SSO existe) -- ❌ Mauvaise UX - -**Login après logout** : -- ⚠️ Peut auto-login si session SSO Keycloak existe -- ⚠️ Logique complexe de détection - -**Logout** : -- ✅ Fonctionne mais peut laisser session SSO active - -**Code** : -- ❌ Logique complexe dans signin/page.tsx -- ❌ Race conditions possibles - -### Après les modifications - -**Login première visite** : -- ✅ SSO fonctionne naturellement (pas de prompt si session existe) -- ✅ Meilleure UX - -**Login après logout** : -- ✅ Session SSO terminée via `end-sso-session` (Admin API) -- ✅ Cookie `force_login_prompt` marque le logout -- ⚠️ Note: `prompt=login` n'est pas encore ajouté dynamiquement (limitation NextAuth) -- ✅ Mais session SSO est terminée donc credentials seront demandés - -**Logout** : -- ✅ Appelle `mark-logout` pour créer cookie serveur -- ✅ Termine session SSO via Admin API -- ✅ Supprime cookies NextAuth -- ✅ Redirige vers Keycloak logout - -**Code** : -- ✅ Logique simplifiée dans signin/page.tsx -- ✅ Moins de race conditions -- ✅ Configuration cookies explicite - ---- - -## 📝 Notes Importantes - -### Limitation Actuelle - -**`prompt=login` dynamique** : -- NextAuth v4 ne permet pas facilement d'ajouter `prompt=login` dynamiquement -- Solution actuelle : Terminer la session SSO via Admin API (`end-sso-session`) -- **Impact** : Si session SSO est bien terminée, Keycloak demandera credentials de toute façon -- **Amélioration future possible** : Middleware Next.js pour intercepter et modifier l'URL Keycloak - -### Solution de Contournement - -Au lieu d'ajouter `prompt=login` dynamiquement, nous : -1. Terminons la session SSO Keycloak via Admin API (`end-sso-logout`) -2. Créons un cookie serveur (`force_login_prompt`) pour tracking -3. Laissons Keycloak gérer naturellement (sans session SSO, il demandera credentials) - ---- - -## 🧪 Tests à Effectuer - -### Test 1 : Login première visite -1. Ouvrir navigateur en navigation privée -2. Aller sur `/signin` -3. **Attendu** : Redirection vers Keycloak, SSO fonctionne si session existe -4. **Résultat** : ✅ SSO fonctionne (pas de prompt forcé) - -### Test 2 : Login après logout -1. Se connecter -2. Se déconnecter -3. Cliquer "Se connecter" -4. **Attendu** : Keycloak demande credentials (session SSO terminée) -5. **Résultat** : ✅ Credentials demandés (session SSO terminée) - -### Test 3 : Logout depuis dashboard -1. Se connecter -2. Cliquer "Déconnexion" dans navigation -3. **Attendu** : Redirection vers `/signin?logout=true` -4. **Résultat** : ✅ Logout fonctionne - -### Test 4 : Logout depuis iframe -1. Se connecter -2. Ouvrir une application en iframe -3. Se déconnecter depuis l'iframe -4. **Attendu** : Dashboard se déconnecte aussi -5. **Résultat** : ✅ Logout synchronisé - -### Test 5 : Expiration session -1. Se connecter -2. Attendre expiration (ou invalider session Keycloak) -3. **Attendu** : Redirection vers `/signin` avec message approprié -4. **Résultat** : ✅ Détection et redirection fonctionnent - ---- - -## 🔧 Prochaines Améliorations Possibles - -### Option 1 : Middleware pour ajouter `prompt=login` -Créer un middleware Next.js qui intercepte `/api/auth/signin/keycloak` et modifie l'URL Keycloak pour ajouter `prompt=login` si cookie `force_login_prompt` existe. - -### Option 2 : Route custom signin -Créer une route custom qui gère complètement le flow OAuth avec `prompt=login` et utilise NextAuth pour valider le callback. - -### Option 3 : Modifier Keycloak configuration -Configurer Keycloak pour toujours demander credentials après logout (configuration côté Keycloak). - ---- - -## 📊 Fichiers Modifiés - -1. ✅ `app/api/auth/options.ts` - Configuration NextAuth -2. ✅ `app/api/auth/mark-logout/route.ts` - Nouveau fichier -3. ✅ `components/auth/signout-handler.tsx` - Handler logout -4. ✅ `components/main-nav.tsx` - Navigation logout -5. ✅ `components/layout/layout-wrapper.tsx` - Layout logout iframe -6. ✅ `app/signin/page.tsx` - Page signin simplifiée - ---- - -## ✅ Résumé - -**8 modifications majeures effectuées** : -- ✅ SSO naturel fonctionne -- ✅ Logout marqué côté serveur -- ✅ Code simplifié -- ✅ Meilleure gestion erreurs -- ✅ Configuration cookies explicite -- ⚠️ `prompt=login` dynamique non implémenté (limitation NextAuth, mais contourné via end-sso-session) - -**Impact global** : ✅ Flow de connexion amélioré, code plus maintenable, meilleure UX - ---- - -**Document créé le** : $(date) - diff --git a/COMPREHENSIVE_NOTIFICATION_ANALYSIS.md b/COMPREHENSIVE_NOTIFICATION_ANALYSIS.md deleted file mode 100644 index 51e29418..00000000 --- a/COMPREHENSIVE_NOTIFICATION_ANALYSIS.md +++ /dev/null @@ -1,789 +0,0 @@ -# Comprehensive Notification System Analysis & Improvement Recommendations - -**Date**: 2026-01-06 -**Purpose**: Complete step-by-step trace of notification system with improvement recommendations - ---- - -## 📋 **Table of Contents** - -1. [Architecture Overview](#architecture-overview) -2. [Complete Flow Traces](#complete-flow-traces) -3. [Current Issues Identified](#current-issues-identified) -4. [Improvement Recommendations](#improvement-recommendations) -5. [Performance Optimizations](#performance-optimizations) -6. [Reliability Improvements](#reliability-improvements) -7. [User Experience Enhancements](#user-experience-enhancements) - ---- - -## 🏗️ **Architecture Overview** - -### **Components**: - -``` -┌─────────────────────────────────────────────────────────────┐ -│ UI Layer (React) │ -│ ┌─────────────────────────────────────────────────────┐ │ -│ │ NotificationBadge Component │ │ -│ │ - Displays notification count badge │ │ -│ │ - Dropdown with notification list │ │ -│ │ - Mark as read / Mark all as read buttons │ │ -│ └─────────────────────────────────────────────────────┘ │ -│ ↓ │ -│ ┌─────────────────────────────────────────────────────┐ │ -│ │ useNotifications Hook │ │ -│ │ - State management (notifications, count, loading) │ │ -│ │ - Polling (60s interval) │ │ -│ │ - Optimistic updates │ │ -│ │ - Rate limiting (5s minimum between fetches) │ │ -│ └─────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ API Routes (Next.js) │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ GET /count │ │ GET /list │ │ POST /read │ │ -│ │ │ │ │ │ POST /read-all│ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Service Layer (NotificationService) │ -│ - Singleton pattern │ -│ - Adapter pattern (LeantimeAdapter, future adapters) │ -│ - Redis caching (count: 30s, list: 5min) │ -│ - Cache invalidation │ -│ - Background refresh scheduling │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Adapter Layer (LeantimeAdapter) │ -│ - User ID caching (1 hour TTL) │ -│ - Retry logic (3 attempts, exponential backoff) │ -│ - Direct API calls to Leantime │ -│ - Notification transformation │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ External API (Leantime) │ -│ - JSON-RPC API │ -│ - getAllNotifications, markNotificationRead, etc. │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 🔄 **Complete Flow Traces** - -### **Flow 1: Initial Page Load & Count Display** - -#### **Step-by-Step**: - -1. **Component Mount** (`notification-badge.tsx`) - ``` - - Component renders - - useNotifications() hook initializes - - useEffect triggers when status === 'authenticated' - ``` - -2. **Hook Initialization** (`use-notifications.ts`) - ``` - - Sets isMountedRef.current = true - - Calls fetchNotificationCount(true) - force refresh - - Calls fetchNotifications(1, 20) - - Starts polling: setInterval every 60 seconds - ``` - -3. **Count Fetch** (`use-notifications.ts` → `/api/notifications/count`) - ``` - - Checks: session exists, isMounted, rate limit (5s) - - Makes GET request: /api/notifications/count?_t=${Date.now()} - - Cache-busting parameter added - ``` - -4. **API Route** (`app/api/notifications/count/route.ts`) - ``` - - Authenticates user via getServerSession() - - Gets userId from session - - Calls NotificationService.getNotificationCount(userId) - ``` - -5. **Service Layer** (`notification-service.ts`) - ``` - - Checks Redis cache: notifications:count:${userId} - - If cached: Returns cached data (30s TTL) - - If not cached: Fetches from adapters - ``` - -6. **Adapter Layer** (`leantime-adapter.ts`) - ``` - - getNotificationCount() called - - Gets user email from session - - Gets Leantime user ID (checks cache first, then API with retry) - - Fetches up to 1000 notifications directly from API - - Counts unread: filter(n => n.read === 0) - - Returns count object - ``` - -7. **Cache Storage** (`notification-service.ts`) - ``` - - Stores count in Redis: notifications:count:${userId} - - TTL: 30 seconds - - Returns to API route - ``` - -8. **Response** (`app/api/notifications/count/route.ts`) - ``` - - Returns JSON with count - - Sets Cache-Control: private, max-age=10 - ``` - -9. **Hook Update** (`use-notifications.ts`) - ``` - - Receives count data - - Updates state: setNotificationCount(data) - ``` - -10. **UI Update** (`notification-badge.tsx`) - ``` - - Badge displays notificationCount.unread - - Shows "60" if 60 unread notifications - ``` - ---- - -### **Flow 2: Mark All Notifications as Read** - -#### **Step-by-Step**: - -1. **User Action** (`notification-badge.tsx`) - ``` - - User clicks "Mark all read" button - - Calls handleMarkAllAsRead() - - Calls markAllAsRead() from hook - ``` - -2. **Optimistic Update** (`use-notifications.ts`) - ``` - - Immediately updates state: - * All notifications: isRead = true - * Count: unread = 0 - - Provides instant UI feedback - ``` - -3. **API Call** (`use-notifications.ts`) - ``` - - Makes POST to /api/notifications/read-all - - Waits for response - ``` - -4. **API Route** (`app/api/notifications/read-all/route.ts`) - ``` - - Authenticates user - - Calls NotificationService.markAllAsRead(userId) - - Logs duration - ``` - -5. **Service Layer** (`notification-service.ts`) - ``` - - Loops through all adapters - - For each adapter: - * Checks if configured - * Calls adapter.markAllAsRead(userId) - - Collects results - - Always invalidates cache (even on failure) - ``` - -6. **Adapter Layer** (`leantime-adapter.ts`) - ``` - - Gets user email from session - - Gets Leantime user ID (cached or fetched with retry) - - Fetches all notifications from API (up to 1000) - - Filters unread: filter(n => n.read === 0) - - Marks each individually using Promise.all() - - Returns success if any were marked - ``` - -7. **Cache Invalidation** (`notification-service.ts`) - ``` - - Deletes count cache: notifications:count:${userId} - - Deletes all list caches: notifications:list:${userId}:* - - Uses SCAN to avoid blocking Redis - ``` - -8. **Count Refresh** (`use-notifications.ts`) - ``` - - After 200ms delay, calls fetchNotificationCount(true) - - Fetches fresh count from API - - Updates state with new count - ``` - ---- - -### **Flow 3: Polling for Updates** - -#### **Step-by-Step**: - -1. **Polling Setup** (`use-notifications.ts`) - ``` - - setInterval created: 60 seconds - - Calls debouncedFetchCount() on each interval - ``` - -2. **Debounced Fetch** (`use-notifications.ts`) - ``` - - Debounce delay: 300ms - - Prevents rapid successive calls - - Calls fetchNotificationCount(false) - ``` - -3. **Rate Limiting** (`use-notifications.ts`) - ``` - - Checks: now - lastFetchTime < 5 seconds - - If too soon, skips fetch - ``` - -4. **Count Fetch** (same as Flow 1, steps 3-10) - ``` - - Fetches from API - - Updates count if changed - ``` - ---- - -## 🐛 **Current Issues Identified** - -### **Issue #1: Multiple Fetching Mechanisms** - -**Problem**: -- `useNotifications` has its own polling (60s) -- `NotificationService` has background refresh -- `NotificationBadge` has manual fetch on open -- No coordination between them - -**Impact**: -- Redundant API calls -- Inconsistent refresh timing -- Potential race conditions - ---- - -### **Issue #2: Mark All As Read - Sequential Processing** - -**Problem**: -- Marks all notifications in parallel using `Promise.all()` -- No batching or rate limiting -- Can overwhelm Leantime API -- Connection resets on large batches (60+ notifications) - -**Impact**: -- Partial failures (some marked, some not) -- Network timeouts -- Poor user experience - ---- - -### **Issue #3: Cache TTL Mismatch** - -**Problem**: -- Count cache: 30 seconds -- List cache: 5 minutes -- Client cache: 10 seconds (count), 30 seconds (list) -- Background refresh: 1 minute cooldown - -**Impact**: -- Stale data inconsistencies -- Count and list can be out of sync -- Confusing UX - ---- - -### **Issue #4: No Progress Feedback** - -**Problem**: -- Mark all as read shows no progress -- User doesn't know how many are being marked -- No indication if operation is still running - -**Impact**: -- Poor UX -- User might click multiple times -- No way to cancel operation - ---- - -### **Issue #5: Optimistic Updates Can Be Wrong** - -**Problem**: -- Hook optimistically sets count to 0 -- But operation might fail or be partial -- Count refresh after 200ms might show different value -- Count jumps: 60 → 0 → 40 (confusing) - -**Impact**: -- Confusing UX -- User thinks operation failed when it partially succeeded - ---- - -### **Issue #6: No Retry for Mark All As Read** - -**Problem**: -- If connection resets during marking, operation fails -- No automatic retry for failed notifications -- User must manually retry - -**Impact**: -- Partial success requires manual intervention -- Poor reliability - ---- - -### **Issue #7: Session Lookup on Every Call** - -**Problem**: -- `getUserEmail()` calls `getServerSession()` every time -- `getLeantimeUserId()` is cached, but email lookup is not -- Multiple session lookups per request - -**Impact**: -- Performance overhead -- Potential session inconsistencies - ---- - -### **Issue #8: No Connection Pooling** - -**Problem**: -- Each API call creates new fetch request -- No connection reuse -- No request queuing - -**Impact**: -- Slower performance -- Higher connection overhead -- Potential connection exhaustion - ---- - -### **Issue #9: Background Refresh Uses setTimeout** - -**Problem**: -- `scheduleBackgroundRefresh()` uses `setTimeout(0)` -- Not reliable in serverless environments -- Can be lost if server restarts - -**Impact**: -- Background refresh might not happen -- Cache might become stale - ---- - -### **Issue #10: No Unified Refresh Integration** - -**Problem**: -- `useNotifications` has its own polling -- `RefreshManager` exists but not used -- `useUnifiedRefresh` hook exists but not integrated - -**Impact**: -- Duplicate refresh logic -- Inconsistent refresh intervals -- Not using centralized refresh system - ---- - -## 💡 **Improvement Recommendations** - -### **Priority 1: Integrate Unified Refresh System** - -**Current State**: -- `useNotifications` has custom polling (60s) -- `RefreshManager` exists but not used -- `useUnifiedRefresh` hook exists but not integrated - -**Recommendation**: -- Replace custom polling with `useUnifiedRefresh` -- Use `REFRESH_INTERVALS.NOTIFICATIONS_COUNT` (30s) -- Remove duplicate polling logic -- Centralize all refresh management - -**Benefits**: -- ✅ Consistent refresh intervals -- ✅ Reduced code duplication -- ✅ Better coordination with other widgets -- ✅ Easier to manage globally - ---- - -### **Priority 2: Batch Mark All As Read** - -**Current State**: -- Marks all notifications in parallel -- No batching or rate limiting -- Can overwhelm API - -**Recommendation**: -- Process in batches of 10-20 notifications -- Add delay between batches (100-200ms) -- Show progress indicator -- Retry failed batches automatically - -**Implementation**: -```typescript -// Pseudo-code -async markAllAsRead(userId: string): Promise { - const BATCH_SIZE = 10; - const BATCH_DELAY = 200; - - const batches = chunk(unreadNotifications, BATCH_SIZE); - - for (const batch of batches) { - await Promise.all(batch.map(n => markAsRead(n.id))); - await delay(BATCH_DELAY); - // Update progress - } -} -``` - -**Benefits**: -- ✅ Prevents API overload -- ✅ Better error recovery -- ✅ Progress feedback -- ✅ More reliable - ---- - -### **Priority 3: Fix Cache TTL Consistency** - -**Current State**: -- Count cache: 30s -- List cache: 5min -- Client cache: 10s/30s -- Background refresh: 1min - -**Recommendation**: -- Align all cache TTLs -- Count cache: 30s (matches refresh interval) -- List cache: 30s (same as count) -- Client cache: 0s (rely on server cache) -- Background refresh: 30s (matches TTL) - -**Benefits**: -- ✅ Consistent data -- ✅ Count and list always in sync -- ✅ Predictable behavior - ---- - -### **Priority 4: Add Progress Feedback** - -**Current State**: -- No progress indication -- User doesn't know operation status - -**Recommendation**: -- Show progress bar: "Marking X of Y..." -- Update in real-time as batches complete -- Show success/failure count -- Allow cancellation - -**Benefits**: -- ✅ Better UX -- ✅ User knows what's happening -- ✅ Prevents multiple clicks - ---- - -### **Priority 5: Improve Optimistic Updates** - -**Current State**: -- Optimistically sets count to 0 -- Might be wrong if operation fails -- Count jumps confusingly - -**Recommendation**: -- Only show optimistic update if confident -- Show loading state instead of immediate 0 -- Poll until count matches expected value -- Or: Show "Marking..." state instead of 0 - -**Benefits**: -- ✅ More accurate UI -- ✅ Less confusing -- ✅ Better error handling - ---- - -### **Priority 6: Add Automatic Retry** - -**Current State**: -- No retry for failed notifications -- User must manually retry - -**Recommendation**: -- Track which notifications failed -- Automatically retry failed ones -- Exponential backoff -- Max 3 retries per notification - -**Benefits**: -- ✅ Better reliability -- ✅ Automatic recovery -- ✅ Less manual intervention - ---- - -### **Priority 7: Cache User Email** - -**Current State**: -- `getUserEmail()` calls session every time -- Not cached - -**Recommendation**: -- Cache user email in Redis (same TTL as user ID) -- Invalidate on session change -- Reduce session lookups - -**Benefits**: -- ✅ Better performance -- ✅ Fewer session calls -- ✅ More consistent - ---- - -### **Priority 8: Add Connection Pooling** - -**Current State**: -- Each API call creates new fetch -- No connection reuse - -**Recommendation**: -- Use HTTP agent with connection pooling -- Reuse connections -- Queue requests if needed - -**Benefits**: -- ✅ Better performance -- ✅ Lower overhead -- ✅ More reliable connections - ---- - -### **Priority 9: Replace setTimeout with Proper Scheduling** - -**Current State**: -- Background refresh uses `setTimeout(0)` -- Not reliable in serverless - -**Recommendation**: -- Use proper job queue (Bull, Agenda, etc.) -- Or: Use Next.js API route for background jobs -- Or: Use cron job for scheduled refreshes - -**Benefits**: -- ✅ More reliable -- ✅ Works in serverless -- ✅ Better error handling - ---- - -### **Priority 10: Add Request Deduplication** - -**Current State**: -- Multiple components can trigger same fetch -- No deduplication - -**Recommendation**: -- Use `requestDeduplicator` utility (already exists) -- Deduplicate identical requests within short window -- Share results between callers - -**Benefits**: -- ✅ Fewer API calls -- ✅ Better performance -- ✅ Reduced server load - ---- - -## ⚡ **Performance Optimizations** - -### **1. Reduce API Calls** - -**Current**: -- Polling every 60s -- Background refresh every 1min -- Manual fetch on dropdown open -- Count refresh after marking - -**Optimization**: -- Use unified refresh (30s) -- Deduplicate requests -- Share cache between components -- Reduce redundant fetches - -**Expected Improvement**: 50-70% reduction in API calls - ---- - -### **2. Optimize Mark All As Read** - -**Current**: -- All notifications in parallel -- No batching -- Can timeout - -**Optimization**: -- Batch processing (10-20 at a time) -- Delay between batches -- Progress tracking -- Automatic retry - -**Expected Improvement**: 80-90% success rate (vs current 60-70%) - ---- - -### **3. Improve Cache Strategy** - -**Current**: -- Inconsistent TTLs -- Separate caches -- No coordination - -**Optimization**: -- Unified TTLs -- Coordinated invalidation -- Cache versioning -- Smart refresh - -**Expected Improvement**: 30-40% faster response times - ---- - -## 🛡️ **Reliability Improvements** - -### **1. Better Error Handling** - -**Current**: -- Basic try/catch -- Returns false on error -- No retry logic - -**Improvement**: -- Retry with exponential backoff -- Circuit breaker pattern -- Graceful degradation -- Better error messages - ---- - -### **2. Connection Resilience** - -**Current**: -- Fails on connection reset -- No recovery - -**Improvement**: -- Automatic retry -- Connection pooling -- Health checks -- Fallback mechanisms - ---- - -### **3. Partial Failure Handling** - -**Current**: -- All-or-nothing approach -- No tracking of partial success - -**Improvement**: -- Track which notifications succeeded -- Retry only failed ones -- Report partial success -- Allow resume - ---- - -## 🎨 **User Experience Enhancements** - -### **1. Progress Indicators** - -- Show "Marking X of Y..." during mark all -- Progress bar -- Success/failure count -- Estimated time remaining - ---- - -### **2. Better Loading States** - -- Skeleton loaders -- Optimistic updates with loading overlay -- Smooth transitions -- No jarring count jumps - ---- - -### **3. Error Messages** - -- User-friendly error messages -- Actionable suggestions -- Retry buttons -- Help text - ---- - -### **4. Real-time Updates** - -- WebSocket/SSE for real-time updates -- Instant count updates -- No polling needed -- Better UX - ---- - -## 📊 **Summary of Improvements** - -### **High Priority** (Implement First): -1. ✅ Integrate unified refresh system -2. ✅ Batch mark all as read -3. ✅ Fix cache TTL consistency -4. ✅ Add progress feedback - -### **Medium Priority**: -5. ✅ Improve optimistic updates -6. ✅ Add automatic retry -7. ✅ Cache user email -8. ✅ Add request deduplication - -### **Low Priority** (Nice to Have): -9. ✅ Connection pooling -10. ✅ Replace setTimeout with proper scheduling -11. ✅ WebSocket/SSE for real-time updates - ---- - -## 🎯 **Expected Results After Improvements** - -### **Performance**: -- 50-70% reduction in API calls -- 30-40% faster response times -- 80-90% success rate for mark all - -### **Reliability**: -- Automatic retry for failures -- Better error recovery -- More consistent behavior - -### **User Experience**: -- Progress indicators -- Better loading states -- Clearer error messages -- Smoother interactions - ---- - -**Status**: Analysis complete. Ready for implementation prioritization. - diff --git a/COURRIER_USER_MANAGEMENT.md b/COURRIER_USER_MANAGEMENT.md deleted file mode 100644 index 5ee71813..00000000 --- a/COURRIER_USER_MANAGEMENT.md +++ /dev/null @@ -1,304 +0,0 @@ -# Courrier User Management with Prisma - -## Overview - -**Important**: Courrier (the email system) does **NOT** create User records in Prisma. It only manages email account credentials (`MailCredentials`) for users that already exist in the database. - -## User Creation Flow - -### 1. User Creation in Keycloak (Primary Source) - -Users are created in **Keycloak** first, which is the primary authentication system: - -**Location**: `app/api/users/route.ts` (POST method) - -**Process**: -1. User is created in Keycloak via Admin API -2. Roles are assigned to the user -3. User may be created in external systems: - - **Leantime** (project management tool) - - **Dolibarr** (if user has "Mediation" or "Expression" roles) - -**Key Code**: -```typescript -// Create user in Keycloak -const createResponse = await fetch( - `${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users`, - { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username: data.username, - enabled: true, - emailVerified: true, - firstName: data.firstName, - lastName: data.lastName, - email: data.email, - credentials: [{ type: "password", value: data.password, temporary: false }], - }), - } -); -``` - -### 2. User Sync to Prisma Database - -After creation in Keycloak, users need to be synced to the Prisma database. This happens via: - -**Option A: Manual Sync Script** -- `scripts/sync-users.ts` or `scripts/sync-users.js` -- Fetches users from Keycloak API -- Creates/updates User records in Prisma - -**Option B: API Endpoint** -- `app/api/sync-users/route.ts` (GET method) -- Can be called to sync users programmatically - -**Prisma User Creation**: -```typescript -await prisma.user.create({ - data: { - id: user.id, // Use the Keycloak ID as primary ID - email: user.email, - password: tempPassword, // Temporary password (not used for auth) - createdAt: new Date(), - updatedAt: new Date(), - }, -}); -``` - -**Important Notes**: -- The Prisma User `id` field uses the **Keycloak user ID** (UUID) -- The `password` field in Prisma is not used for authentication (Keycloak handles that) -- Users must exist in Prisma before they can use Courrier - -### 3. Prisma Schema - -**User Model** (`prisma/schema.prisma`): -```prisma -model User { - id String @id @default(uuid()) - email String @unique - password String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - mailCredentials MailCredentials[] // One-to-many relationship - // ... other relations -} -``` - -**MailCredentials Model**: -```prisma -model MailCredentials { - id String @id @default(uuid()) - userId String - email String - password String? // Optional (for OAuth accounts) - host String - port Int - secure Boolean @default(true) - use_oauth Boolean @default(false) - refresh_token String? - access_token String? - token_expiry DateTime? - smtp_host String? - smtp_port Int? - smtp_secure Boolean? @default(false) - display_name String? - color String? @default("#0082c9") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([userId, email]) // One email account per user - @@index([userId]) -} -``` - -## Courrier's Role: Adding Email Accounts - -### How Courrier Adds Email Accounts - -**Location**: `app/api/courrier/account/route.ts` (POST method) - -**Process**: -1. **Authentication Check**: Verifies user session exists -2. **User Existence Check**: Verifies user exists in Prisma database -3. **Connection Test**: Tests IMAP connection before saving -4. **Save Credentials**: Creates/updates `MailCredentials` record - -**Key Code Flow**: - -```typescript -// 1. Check if user exists in database -const userExistsInDB = await userExists(session.user.id); -if (!userExistsInDB) { - return NextResponse.json({ - error: 'User not found in database', - details: `The user ID from your session (${session.user.id}) doesn't exist in the database.` - }, { status: 400 }); -} - -// 2. Test connection -const testResult = await testEmailConnection(credentials); -if (!testResult.imap) { - return NextResponse.json({ - error: `Connection test failed: ${testResult.error}` - }, { status: 400 }); -} - -// 3. Save credentials -await saveUserEmailCredentials(session.user.id, email, credentials); -``` - -### Saving Email Credentials - -**Location**: `lib/services/email-service.ts` → `saveUserEmailCredentials()` - -**Process**: -1. Prepares database credentials object (excluding OAuth tokens) -2. Uses `upsert` to create or update `MailCredentials` -3. Caches full credentials (including OAuth tokens) in Redis - -**Key Code**: -```typescript -// Save to database using upsert -await prisma.mailCredentials.upsert({ - where: { - // Finds existing record by userId + email - userId_email: { - userId: userId, - email: credentials.email - } - }, - update: dbCredentials, - create: { - userId, - ...dbCredentials - } -}); - -// Cache full credentials (including OAuth) in Redis -await cacheEmailCredentials(userId, accountId, fullCreds); -``` - -**Important Notes**: -- OAuth tokens (access_token, refresh_token) are stored in **Redis only**, not in Prisma -- The Prisma `MailCredentials` table stores IMAP/SMTP settings -- The `password` field is optional (for OAuth accounts like Microsoft) - -### Microsoft OAuth Flow - -**Location**: `app/api/courrier/microsoft/callback/route.ts` - -For Microsoft accounts, the flow is: -1. User authorizes via Microsoft OAuth -2. Access token and refresh token are obtained -3. Credentials are saved with `use_oauth: true` -4. OAuth tokens are cached in Redis (not in Prisma) - -## Data Flow Diagram - -``` -┌─────────────┐ -│ Keycloak │ ← Primary user creation -└──────┬──────┘ - │ - │ Sync - ↓ -┌─────────────┐ -│ Prisma │ ← User record created -│ User │ -└──────┬──────┘ - │ - │ User adds email account - ↓ -┌─────────────┐ -│ Prisma │ ← MailCredentials created -│MailCredentials│ -└──────┬──────┘ - │ - │ OAuth tokens (if applicable) - ↓ -┌─────────────┐ -│ Redis │ ← OAuth tokens cached -└─────────────┘ -``` - -## Key Files Reference - -### User Creation -- `app/api/users/route.ts` - Creates users in Keycloak -- `scripts/sync-users.ts` - Syncs users from Keycloak to Prisma -- `app/api/sync-users/route.ts` - API endpoint for syncing users - -### Courrier Email Management -- `app/api/courrier/account/route.ts` - Add/update/delete email accounts -- `lib/services/email-service.ts` - Core email service functions - - `saveUserEmailCredentials()` - Saves email credentials to Prisma - - `getUserEmailCredentials()` - Retrieves credentials from Prisma - - `testEmailConnection()` - Tests IMAP/SMTP connection - -### Database Schema -- `prisma/schema.prisma` - Prisma schema definitions -- `lib/prisma.ts` - Prisma client instance - -### Authentication -- `app/api/auth/options.ts` - NextAuth configuration -- `lib/auth.ts` - Authentication helpers - -## Auto-Creation of Users - -**As of recent updates**, Courrier now automatically creates User records in Prisma if they don't exist when: -- Adding an email account (`/api/courrier/account` POST) -- Checking session status (`/api/courrier/session` GET) - -This handles cases where: -- The database was reset/lost but users still exist in Keycloak -- Users were created in Keycloak but never synced to Prisma - -The auto-creation uses session data from Keycloak to populate: -- `id`: Keycloak user ID (UUID) -- `email`: User's email from session -- `password`: Temporary random password (not used for auth, Keycloak handles authentication) - -## Common Issues & Solutions - -### Issue: "User not found in database" when adding email account - -**Cause**: User exists in Keycloak but not in Prisma database - -**Solution**: -- **Automatic**: The system now auto-creates users when needed -- **Manual**: Run the sync script to create users in Prisma: -```bash -npm run sync-users -# or -node scripts/sync-users.js -``` - -### Issue: Email credentials not saving - -**Check**: -1. User exists in Prisma: `prisma.user.findUnique({ where: { id: userId } })` -2. Connection test passes before saving -3. Unique constraint `[userId, email]` is not violated - -### Issue: OAuth tokens not persisting - -**Note**: OAuth tokens are stored in Redis, not Prisma. Check: -- Redis connection and TTL settings -- Redis cache functions in `lib/redis.ts` - -## Summary - -1. **Users are created in Keycloak first** (via `app/api/users/route.ts`) -2. **Users are synced to Prisma** (via sync scripts or API) -3. **Courrier adds email accounts** by creating `MailCredentials` records linked to existing Users -4. **OAuth tokens are cached in Redis**, not stored in Prisma -5. **Users must exist in Prisma** before they can add email accounts via Courrier - -Courrier is a **credentials management system** for existing users, not a user creation system. - diff --git a/CRITICAL_FIXES_QUICK_REFERENCE.md b/CRITICAL_FIXES_QUICK_REFERENCE.md deleted file mode 100644 index 6008c0b0..00000000 --- a/CRITICAL_FIXES_QUICK_REFERENCE.md +++ /dev/null @@ -1,307 +0,0 @@ -# Critical Fixes - Quick Reference Guide - -## 🚨 Top 5 Critical Fixes (Do These First) - -### 1. Fix useNotifications Memory Leak ⚠️ CRITICAL - -**File**: `hooks/use-notifications.ts` -**Line**: 239-255 - -**Problem**: Cleanup function not properly placed, causing memory leaks - -**Quick Fix**: -```typescript -useEffect(() => { - if (status !== 'authenticated' || !session?.user) return; - - isMountedRef.current = true; - - // Initial fetch - fetchNotificationCount(true); - fetchNotifications(); - - // Start polling with proper cleanup - const intervalId = setInterval(() => { - if (isMountedRef.current) { - debouncedFetchCount(); - } - }, POLLING_INTERVAL); - - // ✅ Proper cleanup - return () => { - isMountedRef.current = false; - clearInterval(intervalId); - }; -}, [status, session?.user?.id]); // ✅ Only primitive dependencies -``` - ---- - -### 2. Fix Notification Badge Double Fetching ⚠️ CRITICAL - -**File**: `components/notification-badge.tsx` -**Lines**: 65-70, 82-87, 92-99 - -**Problem**: Three different places trigger the same fetch simultaneously - -**Quick Fix**: -```typescript -// Add at top of component -const fetchInProgressRef = useRef(false); -const lastFetchRef = useRef(0); -const FETCH_COOLDOWN = 1000; // 1 second cooldown - -const manualFetch = async () => { - const now = Date.now(); - - // Prevent duplicate fetches - if (fetchInProgressRef.current) { - console.log('[NOTIFICATION_BADGE] Fetch already in progress'); - return; - } - - // Cooldown check - if (now - lastFetchRef.current < FETCH_COOLDOWN) { - console.log('[NOTIFICATION_BADGE] Too soon since last fetch'); - return; - } - - fetchInProgressRef.current = true; - lastFetchRef.current = now; - - try { - await fetchNotifications(1, 10); - } finally { - fetchInProgressRef.current = false; - } -}; - -// Remove duplicate useEffect hooks, keep only one: -useEffect(() => { - if (isOpen && status === 'authenticated') { - manualFetch(); - } -}, [isOpen, status]); // Only this one -``` - ---- - -### 3. Fix Redis KEYS Performance Issue ⚠️ CRITICAL - -**File**: `lib/services/notifications/notification-service.ts` -**Line**: 293 - -**Problem**: `redis.keys()` blocks Redis and is O(N) - -**Quick Fix**: -```typescript -// BEFORE (Line 293) -const listKeys = await redis.keys(listKeysPattern); -if (listKeys.length > 0) { - await redis.del(...listKeys); -} - -// AFTER (Use SCAN) -const listKeys: string[] = []; -let cursor = '0'; -do { - const [nextCursor, keys] = await redis.scan( - cursor, - 'MATCH', - listKeysPattern, - 'COUNT', - 100 - ); - cursor = nextCursor; - if (keys.length > 0) { - listKeys.push(...keys); - } -} while (cursor !== '0'); - -if (listKeys.length > 0) { - await redis.del(...listKeys); -} -``` - ---- - -### 4. Fix Widget Interval Cleanup ⚠️ HIGH - -**Files**: -- `components/calendar.tsx` (line 70) -- `components/parole.tsx` (line 83) -- `components/calendar/calendar-widget.tsx` (line 110) - -**Problem**: Intervals may not be cleaned up properly - -**Quick Fix Pattern**: -```typescript -// BEFORE -useEffect(() => { - fetchEvents(); - const intervalId = setInterval(fetchEvents, 300000); - return () => clearInterval(intervalId); -}, []); // ❌ Missing dependencies - -// AFTER -useEffect(() => { - if (status !== 'authenticated') return; - - const fetchEvents = async () => { - // ... fetch logic - }; - - fetchEvents(); // Initial fetch - - const intervalId = setInterval(fetchEvents, 300000); - - return () => { - clearInterval(intervalId); - }; -}, [status]); // ✅ Proper dependencies -``` - ---- - -### 5. Fix useEffect Infinite Loop Risk ⚠️ HIGH - -**File**: `hooks/use-notifications.ts` -**Line**: 255 - -**Problem**: Function dependencies cause infinite re-renders - -**Quick Fix**: -```typescript -// Remove function dependencies, use refs for stable references -const fetchNotificationCountRef = useRef(fetchNotificationCount); -const fetchNotificationsRef = useRef(fetchNotifications); - -useEffect(() => { - fetchNotificationCountRef.current = fetchNotificationCount; - fetchNotificationsRef.current = fetchNotifications; -}); - -useEffect(() => { - if (status !== 'authenticated' || !session?.user) return; - - isMountedRef.current = true; - - fetchNotificationCountRef.current(true); - fetchNotificationsRef.current(); - - const intervalId = setInterval(() => { - if (isMountedRef.current) { - fetchNotificationCountRef.current(); - } - }, POLLING_INTERVAL); - - return () => { - isMountedRef.current = false; - clearInterval(intervalId); - }; -}, [status, session?.user?.id]); // ✅ Only primitive values -``` - ---- - -## 🔧 Additional Quick Wins - -### 6. Add Request Deduplication Utility - -**Create**: `lib/utils/request-deduplication.ts` - -```typescript -const pendingRequests = new Map>(); - -export function deduplicateRequest( - key: string, - requestFn: () => Promise -): Promise { - if (pendingRequests.has(key)) { - return pendingRequests.get(key)!; - } - - const promise = requestFn().finally(() => { - pendingRequests.delete(key); - }); - - pendingRequests.set(key, promise); - return promise; -} -``` - -**Usage**: -```typescript -const data = await deduplicateRequest( - `notifications-${userId}`, - () => fetch('/api/notifications').then(r => r.json()) -); -``` - ---- - -### 7. Extract Magic Numbers to Constants - -**Create**: `lib/constants/intervals.ts` - -```typescript -export const INTERVALS = { - NOTIFICATION_POLLING: 60000, // 1 minute - CALENDAR_REFRESH: 300000, // 5 minutes - PAROLE_POLLING: 30000, // 30 seconds - MIN_FETCH_INTERVAL: 5000, // 5 seconds - FETCH_COOLDOWN: 1000, // 1 second -} as const; -``` - ---- - -### 8. Add Error Retry Logic - -**Create**: `lib/utils/retry.ts` - -```typescript -export async function retry( - fn: () => Promise, - maxAttempts = 3, - delay = 1000 -): Promise { - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - return await fn(); - } catch (error) { - if (attempt === maxAttempts) throw error; - await new Promise(resolve => setTimeout(resolve, delay * attempt)); - } - } - throw new Error('Max retry attempts reached'); -} -``` - ---- - -## 📋 Testing Checklist - -After applying fixes, test: - -- [ ] No memory leaks (check browser DevTools Memory tab) -- [ ] No duplicate API calls (check Network tab) -- [ ] Intervals are cleaned up (check console for errors) -- [ ] No infinite loops (check React DevTools Profiler) -- [ ] Redis performance (check response times) -- [ ] Error handling works (test with network offline) - ---- - -## 🎯 Priority Order - -1. **Fix 1** (Memory Leak) - Do immediately -2. **Fix 2** (Double Fetching) - Do immediately -3. **Fix 3** (Redis KEYS) - Do immediately -4. **Fix 4** (Widget Cleanup) - Do within 24 hours -5. **Fix 5** (Infinite Loop) - Do within 24 hours -6. **Quick Wins** - Do within 1 week - ---- - -*Last Updated: Critical fixes quick reference* diff --git a/DEBUG_502_CALLBACK.md b/DEBUG_502_CALLBACK.md deleted file mode 100644 index 109a0523..00000000 --- a/DEBUG_502_CALLBACK.md +++ /dev/null @@ -1,224 +0,0 @@ -# Debug Erreur 502 - Callback Keycloak - -## 🔍 Situation Actuelle - -**URL** : `https://hub.slm-lab.net/api/auth/callback/keycloak?...` - -**Logs observés** : -- ✅ Profile callback : OK -- ✅ JWT callback : OK (rôles extraits depuis access token) -- ❌ Session callback : **PAS DE LOGS** (ne s'exécute pas ou échoue silencieusement) -- ❌ Erreur 502 Nginx - -## 🎯 Hypothèses - -### Hypothèse 1 : Session callback échoue silencieusement -Le session callback pourrait échouer avant d'atteindre les logs, causant une exception non gérée. - -### Hypothèse 2 : Problème avec token.email ou token.name -Si `token.email` ou `token.name` sont `undefined` et que le code s'attend à des valeurs, cela pourrait causer une erreur. - -### Hypothèse 3 : Timeout ou problème de mémoire -Le callback pourrait prendre trop de temps ou consommer trop de mémoire. - -### Hypothèse 4 : Problème avec NEXTAUTH_URL ou NEXTAUTH_SECRET -Configuration manquante ou incorrecte. - -## ✅ Corrections Appliquées - -### 1. Logs détaillés dans session callback -- Logs au début et à la fin -- Logs de chaque étape -- Logs des valeurs de token - -### 2. Try-catch complet -- Capture toutes les erreurs -- Logs détaillés de l'erreur -- Stack trace complète - -### 3. Validation des champs requis -- Vérification de `token.sub` (user ID) -- Gestion des valeurs `undefined` - -### 4. Events NextAuth -- `signIn` event pour tracker l'authentification -- `error` event pour capturer les erreurs NextAuth -- `signOut` event pour tracking - -## 🔍 Prochaines Étapes d'Investigation - -### Étape 1 : Vérifier les nouveaux logs - -Après redémarrage du serveur, vous devriez voir : - -``` -=== SESSION CALLBACK START === -Token error: undefined -Has accessToken: true -Has refreshToken: true -Token role: [...] -Token sub: ... -... -=== SESSION CALLBACK END === -``` - -**Si vous ne voyez PAS ces logs** : -- Le session callback ne s'exécute pas du tout -- Il y a une erreur avant d'atteindre le callback -- Problème dans NextAuth lui-même - -**Si vous voyez une erreur** : -- Les logs détaillés indiqueront exactement où ça échoue - -### Étape 2 : Vérifier les events NextAuth - -Vous devriez voir : -``` -=== NEXTAUTH SIGNIN EVENT === -User: ... ... -Account: keycloak -Profile: ... -``` - -**Si vous voyez `=== NEXTAUTH ERROR EVENT ===`** : -- L'erreur sera loggée avec détails - -### Étape 3 : Vérifier les variables d'environnement - -**Vérifier dans `.env` ou `.env.local`** : -```bash -NEXTAUTH_URL=https://hub.slm-lab.net -NEXTAUTH_SECRET=... (doit être défini) -KEYCLOAK_ISSUER=https://connect.slm-lab.net/realms/cercle -KEYCLOAK_CLIENT_ID=... -KEYCLOAK_CLIENT_SECRET=... -``` - -**Commandes pour vérifier** : -```bash -# Vérifier que les variables sont chargées -node -e "console.log(process.env.NEXTAUTH_URL)" -node -e "console.log(process.env.NEXTAUTH_SECRET ? 'SET' : 'MISSING')" -``` - -### Étape 4 : Vérifier les logs Nginx - -**Si Nginx est devant Next.js**, vérifier les logs Nginx : -```bash -# Logs d'erreur Nginx -sudo tail -f /var/log/nginx/error.log - -# Logs d'accès Nginx -sudo tail -f /var/log/nginx/access.log -``` - -**Chercher** : -- Timeout errors -- Connection refused -- Upstream errors - -### Étape 5 : Vérifier les logs système - -**Vérifier si Next.js crash** : -```bash -# Logs système -journalctl -u nextjs -f - -# Ou si PM2 -pm2 logs - -# Ou si systemd -systemctl status nextjs -``` - -## 🛠️ Actions Immédiates - -### 1. Redémarrer le serveur Next.js -```bash -# Arrêter -pm2 stop neah -# Ou -systemctl stop nextjs - -# Redémarrer -pm2 start neah -# Ou -systemctl start nextjs -``` - -### 2. Tester à nouveau la connexion - -1. Aller sur `/signin` -2. Se connecter avec Keycloak -3. Observer les logs dans le terminal - -### 3. Partager les logs complets - -**Ce qu'il faut partager** : -- Tous les logs depuis le début de la connexion -- Les logs jusqu'à l'erreur 502 -- Les logs Nginx (si disponibles) -- Les logs système (si disponibles) - -## 🔧 Solutions Possibles - -### Solution 1 : Problème avec token.email ou token.name - -**Si les logs montrent** : -``` -Token email: undefined -Token name: undefined -``` - -**Correction** : Le JWT callback doit extraire email et name depuis le profil ou le token d'accès. - -### Solution 2 : Problème avec NEXTAUTH_URL - -**Si NEXTAUTH_URL est incorrect** : -- NextAuth ne peut pas construire les URLs de callback -- Correction : Vérifier que `NEXTAUTH_URL` correspond à l'URL publique - -### Solution 3 : Problème avec NEXTAUTH_SECRET - -**Si NEXTAUTH_SECRET est manquant** : -- NextAuth ne peut pas signer les JWT -- Correction : Générer un secret et l'ajouter - -### Solution 4 : Timeout - -**Si le callback prend trop de temps** : -- Augmenter les timeouts Nginx -- Optimiser le code du callback - -## 📊 Checklist de Debugging - -- [ ] Serveur Next.js redémarré -- [ ] Logs `=== SESSION CALLBACK START ===` visibles -- [ ] Logs `=== SESSION CALLBACK END ===` visibles -- [ ] Pas d'erreur dans les logs -- [ ] Variables d'environnement vérifiées -- [ ] Logs Nginx vérifiés (si applicable) -- [ ] Logs système vérifiés (si applicable) - -## 🎯 Ce qu'on cherche - -**Dans les prochains logs, on cherche** : - -1. **Si on voit `=== SESSION CALLBACK START ===`** : - - ✅ Le callback s'exécute - - Chercher l'erreur dans les logs suivants - -2. **Si on NE voit PAS `=== SESSION CALLBACK START ===`** : - - ❌ Le callback ne s'exécute pas - - Problème dans NextAuth avant le callback - - Vérifier les events NextAuth - -3. **Si on voit `=== NEXTAUTH ERROR EVENT ===`** : - - ✅ NextAuth a capturé une erreur - - L'erreur sera loggée avec détails - ---- - -**Document créé le** : $(date) -**Statut** : En attente des nouveaux logs après redémarrage - diff --git a/DEPRECATED_FUNCTIONS.md b/DEPRECATED_FUNCTIONS.md deleted file mode 100644 index ef244117..00000000 --- a/DEPRECATED_FUNCTIONS.md +++ /dev/null @@ -1,145 +0,0 @@ -# Deprecated Functions and Files - -This document lists functions and files that have been deprecated and should not be used in new code. - -## Deprecated Files - -### 1. `lib/email-formatter.ts` (REMOVED) -- **Status**: Removed -- **Replacement**: Use `lib/utils/email-utils.ts` instead -- **Reason**: Consolidated email formatting to a single source of truth - -### 2. `lib/mail-parser-wrapper.ts` (REMOVED) -- **Status**: Removed -- **Replacement**: Use functions from `lib/utils/email-utils.ts` instead -- **Reason**: Consolidated email formatting and sanitization to a single source of truth - -### 3. `lib/email-parser.ts` (REMOVED) -- **Status**: Removed -- **Replacement**: Use `lib/server/email-parser.ts` for parsing and `lib/utils/email-utils.ts` for sanitization -- **Reason**: Consolidated email parsing and formatting to dedicated files - -### 4. `lib/compose-mime-decoder.ts` (REMOVED) -- **Status**: Removed -- **Replacement**: Use `decodeComposeContent` and `encodeComposeContent` functions from `lib/utils/email-utils.ts` -- **Reason**: Consolidated MIME handling into the centralized formatter - -## Deprecated Functions - -### 1. `formatEmailForReplyOrForward` in `lib/services/email-service.ts` (REMOVED) -- **Status**: Removed -- **Replacement**: Use `formatEmailForReplyOrForward` from `lib/utils/email-utils.ts` -- **Reason**: Consolidated email formatting to a single source of truth - -### 2. `formatSubject` in `lib/services/email-service.ts` (REMOVED) -- **Status**: Removed -- **Replacement**: None specific, handled by centralized formatter -- **Reason**: Internal function of the email formatter - -### 3. `createQuoteHeader` in `lib/services/email-service.ts` (REMOVED) -- **Status**: Removed -- **Replacement**: None specific, handled by centralized formatter -- **Reason**: Internal function of the email formatter - -## Centralized Email Formatting - -All email formatting is now handled by the centralized formatter in `lib/utils/email-utils.ts`. This file contains: - -1. `formatForwardedEmail`: Format emails for forwarding -2. `formatReplyEmail`: Format emails for replying or replying to all -3. `formatEmailForReplyOrForward`: Compatibility function that maps to the above two -4. `sanitizeHtml`: Safely sanitize HTML content while preserving direction attributes - -Use these functions for all email formatting needs. - -## Email Parsing and Processing Functions - -### 1. `splitEmailHeadersAndBody` (REMOVED) -- **Location**: Removed -- **Reason**: Email parsing has been centralized in `lib/server/email-parser.ts` and the API endpoint. -- **Replacement**: Use the `parseEmail` function from `lib/server/email-parser.ts` which provides a comprehensive parsing solution. - -### 2. `getReplyBody` -- **Location**: `app/courrier/page.tsx` -- **Reason**: Should use the `ReplyContent` component directly. -- **Replacement**: Use `` directly. -- **Status**: Currently marked with `@deprecated` comment, no direct usages found. - -### 3. `generateEmailPreview` -- **Location**: `app/courrier/page.tsx` -- **Reason**: Should use the `EmailPreview` component directly. -- **Replacement**: Use `` directly. -- **Status**: Currently marked with `@deprecated` comment, no usages found. - -### 4. `cleanHtml` (REMOVED) -- **Location**: Removed from `lib/server/email-parser.ts` -- **Reason**: HTML sanitization has been consolidated in `lib/utils/email-utils.ts`. -- **Replacement**: Use `sanitizeHtml` from `lib/utils/email-utils.ts`. - -### 5. `processHtml` (REMOVED) -- **Location**: Removed from `app/api/parse-email/route.ts` -- **Reason**: HTML processing has been consolidated in `lib/utils/email-utils.ts`. -- **Replacement**: Use `sanitizeHtml` from `lib/utils/email-utils.ts`. - -## Deprecated API Routes - -### 1. `app/api/mail/[id]/route.ts` (REMOVED) -- **Status**: Removed -- **Replacement**: Use `app/api/courrier/[id]/route.ts` instead. - -### 2. `app/api/mail/route.ts` (REMOVED) -- **Status**: Removed -- **Replacement**: Use `app/api/courrier/route.ts` instead. - -### 3. `app/api/mail/send/route.ts` (REMOVED) -- **Status**: Removed -- **Replacement**: Use `app/api/courrier/send/route.ts` instead. - -### 4. `DELETE /api/users/[userId]` (DEPRECATED) -- **Status**: Deprecated but maintained for backward compatibility -- **Replacement**: Use `DELETE /api/users?id=[userId]&email=[userEmail]` instead -- **Reason**: The new endpoint format supports deletion across all integrated systems (Keycloak, Leantime, and Dolibarr) -- **Notes**: The deprecated endpoint now forwards requests to the new endpoint but developers should update their code to use the new format directly - -## Deprecated Components - -### ComposeEmail (components/ComposeEmail.tsx) (REMOVED) - -**Status:** Removed -**Replacement:** Use `components/email/ComposeEmail.tsx` instead - -This component has been removed in favor of the more modular and better structured version in the email directory. The newer version has the following improvements: - -- Better separation between user message and quoted content in replies/forwards -- Improved styling and visual hierarchy -- Support for RTL/LTR text direction toggling -- More modern UI using Card components instead of a modal -- Better state management for email composition - -A compatibility layer has been added to the new component to ensure backward compatibility with existing code that uses the old component. This allows for a smooth transition without breaking changes. - -## Migration Plan - -### Phase 1: Deprecation (Completed) -- Mark all deprecated functions with `@deprecated` comments -- Add console warnings to deprecated functions -- Document alternatives - -### Phase 2: Removal (Completed) -- Remove deprecated files: `lib/email-parser.ts` and `lib/mail-parser-wrapper.ts` -- Consolidate all email formatting in `lib/utils/email-utils.ts` -- All email parsing now in `lib/server/email-parser.ts` -- Update documentation to point to the centralized utilities - -## Server-Client Code Separation - -### Server-side imports in client components -- **Status**: Fixed in November 2023 -- **Issue**: Server-only modules like ImapFlow were being imported directly in client components, causing build errors with messages like "Module not found: Can't resolve 'tls'" -- **Fix**: - 1. Added 'use server' directive to server-only modules - 2. Created client-safe interfaces in client components - 3. Added server actions for email operations that need server capabilities - 4. Refactored ComposeEmail component to avoid direct server imports - -This architecture ensures a clean separation between server and client code, which is essential for Next.js applications, particularly with the App Router. It prevents Node.js-specific modules from being bundled into client-side JavaScript. \ No newline at end of file diff --git a/ELECTRON.md b/ELECTRON.md deleted file mode 100644 index c33674ac..00000000 --- a/ELECTRON.md +++ /dev/null @@ -1,64 +0,0 @@ -# Neah Desktop Application - -This is the desktop version of Neah, built with Electron and Next.js. - -## Development - -To run the application in development mode: - -```bash -npm run electron:dev -``` - -This will start the Next.js development server and launch the Electron application that connects to it. - -## Building - -To build installers for your current platform: - -```bash -npm run electron:build -``` - -This will create installers in the `dist` directory. The build script handles: -- Building the Next.js application in static export mode -- Code signing (placeholder for macOS) -- Building installers for your current platform - -## Running from Production Build - -If you have already built the Next.js app and want to run the Electron app without building installers: - -```bash -npm run electron:start -``` - -## Platform-specific Notes - -### macOS - -- The app is configured with a placeholder code signing certificate -- For distribution, you will need to replace this with a real certificate from Apple - -### Windows - -- Windows builds are configured to use NSIS for creating installers - -### Linux - -- Linux builds create AppImage and Debian packages - -## Configuration - -The Electron configuration is in: -- `electron/main.js` - The main Electron process -- `electron/preload.js` - Preload script exposing APIs to the renderer -- `package.json` - Build configuration in the `build` section - -The window controls integration is in: -- `components/electron/WindowControls.tsx` - -## Known Issues - -- API routes: Since this is a static export, any server-side API routes will not work in the Electron app. You'll need to modify API calls to use external services or implement them in Electron. -- Authentication: If using server-side authentication, you may need to implement a custom flow for Electron. \ No newline at end of file diff --git a/IFRAME_LOGOUT_AUTO_LOGIN_ANALYSIS.md b/IFRAME_LOGOUT_AUTO_LOGIN_ANALYSIS.md deleted file mode 100644 index 98e30f87..00000000 --- a/IFRAME_LOGOUT_AUTO_LOGIN_ANALYSIS.md +++ /dev/null @@ -1,259 +0,0 @@ -# Iframe Logout Auto-Login Issue Analysis - -## Problem - -When you log out from an iframe application, you are automatically logged back into the dashboard without being prompted for credentials. - -## Flow Trace - -### Scenario: User Logs Out from Iframe Application - -#### Step 1: Iframe Application Logout -``` -Location: Iframe application (e.g., /parole, /gite, etc.) -Action: User clicks logout in iframe - -What happens: -- Iframe app may call Keycloak logout endpoint directly -- OR: Iframe app sends postMessage to parent: { type: 'KEYCLOAK_LOGOUT' } -- OR: Iframe app clears its own session cookies -``` - -#### Step 2A: If Iframe Sends PostMessage (Expected Flow) -``` -Location: components/layout/layout-wrapper.tsx (line 26-106) -OR: app/components/responsive-iframe.tsx (line 110-153) - -Action: Dashboard receives logout message - -What happens: -1. Sets sessionStorage.setItem('just_logged_out', 'true') -2. Sets document.cookie = 'logout_in_progress=true; path=/; max-age=60' -3. Calls /api/auth/end-sso-session (Admin API) -4. Calls signOut() from NextAuth -5. Redirects to Keycloak logout endpoint -6. Keycloak redirects back to /signin?logout=true -``` - -#### Step 2B: If Iframe Calls Keycloak Logout Directly (Actual Flow - Problem) -``` -Location: Iframe application - -Action: Iframe calls Keycloak logout endpoint directly - -What happens: -1. Iframe redirects to: ${KEYCLOAK_ISSUER}/protocol/openid-connect/logout -2. Keycloak clears session cookies -3. Keycloak may redirect iframe back to its own logout page -4. Dashboard doesn't know about this logout -5. Dashboard still has NextAuth session (valid for 30 days) -``` - -#### Step 3: Dashboard Detects Session Invalidation -``` -Location: app/api/auth/options.ts (refreshAccessToken function) - -When: NextAuth tries to refresh the access token - -What happens: -1. Dashboard calls Keycloak token refresh endpoint -2. Keycloak returns: { error: 'invalid_grant', error_description: 'Session not active' } -3. refreshAccessToken detects this error (line 100-108) -4. Returns token with error: "SessionNotActive" -5. JWT callback clears tokens (line 248-256) -6. Session callback returns null (line 272-276) -7. NextAuth treats user as unauthenticated -8. Status becomes "unauthenticated" -``` - -#### Step 4: Sign-In Page Auto-Login (THE PROBLEM) -``` -Location: app/signin/page.tsx (line 47-79) - -When: User is redirected to /signin (or status becomes "unauthenticated") - -What happens: -1. Component mounts -2. First useEffect (line 16-45) checks for logout flag - - If logout=true in URL, sets isLogoutRedirect.current = true - - Removes 'just_logged_out' from sessionStorage -3. Second useEffect (line 47-79) checks authentication status - - If status === "authenticated" → redirects to home ✅ - - If status === "unauthenticated" → triggers auto-login ❌ - -THE PROBLEM: -- When iframe logs out directly (not via postMessage), dashboard doesn't set logout flags -- Status becomes "unauthenticated" (because Keycloak session was cleared) -- Sign-in page sees status === "unauthenticated" -- Auto-login logic triggers after 1 second (line 69) -- signIn("keycloak") is called -- Keycloak still has SSO session cookie (if it wasn't fully cleared) -- User is auto-authenticated without credentials ❌ -``` - -## Root Cause Analysis - -### Problem 1: Missing Logout Flags - -**When iframe logs out directly (not via postMessage):** -- Dashboard doesn't know about the logout -- `just_logged_out` is NOT set in sessionStorage -- `logout_in_progress` cookie is NOT set -- Sign-in page doesn't know this is a logout scenario - -**Result**: Sign-in page treats it as a normal "unauthenticated" state and triggers auto-login. - -### Problem 2: Auto-Login Logic Timing - -**Sign-in page auto-login logic** (`app/signin/page.tsx:66-78`): -```typescript -if (status === "unauthenticated") { - hasAttemptedLogin.current = true; - const timer = setTimeout(() => { - if (!isLogoutRedirect.current) { - signIn("keycloak", { callbackUrl: "/" }); - } - }, 1000); -} -``` - -**The Issue**: -- `isLogoutRedirect.current` is set in the first useEffect (line 16-45) -- But it only checks for `logout=true` in URL or `just_logged_out` in sessionStorage -- If iframe logs out directly, neither of these is set -- After 1 second, auto-login triggers -- `isLogoutRedirect.current` is still `false` (because logout flags weren't set) -- `signIn("keycloak")` is called -- User is auto-authenticated - -### Problem 3: SSO Session Cookie Persistence - -**Even if logout flags are set correctly:** -- Keycloak SSO session cookie (`KEYCLOAK_SESSION`) may still exist -- When `signIn("keycloak")` is called, Keycloak checks for SSO session cookie -- If cookie exists, Keycloak auto-authenticates without credentials -- This happens even with `prompt=login` parameter (if SSO session is still valid) - -## Why This Happens - -### Flow 1: Iframe Logs Out via PostMessage (Works Correctly) -``` -1. Iframe sends postMessage → Dashboard receives it -2. Dashboard sets logout flags ✅ -3. Dashboard calls logout endpoints ✅ -4. Redirects to /signin?logout=true ✅ -5. Sign-in page sees logout=true ✅ -6. Auto-login is prevented ✅ -7. User must click "Se connecter" manually ✅ -``` - -### Flow 2: Iframe Logs Out Directly (THE PROBLEM) -``` -1. Iframe calls Keycloak logout directly -2. Keycloak clears session cookies -3. Dashboard doesn't know about logout ❌ -4. NextAuth tries to refresh token -5. Keycloak returns "Session not active" -6. NextAuth marks user as unauthenticated -7. User is redirected to /signin (no logout=true) ❌ -8. Sign-in page sees status="unauthenticated" ❌ -9. Auto-login triggers after 1 second ❌ -10. Keycloak still has SSO session cookie ❌ -11. User is auto-authenticated ❌ -``` - -## The Real Issue - -**The sign-in page auto-login logic is too aggressive:** - -1. It triggers auto-login for ANY "unauthenticated" state -2. It doesn't distinguish between: - - User never logged in (should auto-login) ✅ - - User logged out (should NOT auto-login) ❌ - - Session expired (should NOT auto-login) ❌ - - Keycloak session invalidated (should NOT auto-login) ❌ - -3. The logout detection only works if: - - `logout=true` is in URL (from Keycloak redirect) - - `just_logged_out` is in sessionStorage (from dashboard logout) - - But NOT if iframe logs out directly - -## Solution Requirements - -To fix this issue, you need to: - -1. **Detect Keycloak Session Invalidation**: - - When NextAuth detects "SessionNotActive" error - - Set a flag to prevent auto-login - - Mark this as a logout scenario, not a new login - -2. **Improve Logout Detection**: - - Check for Keycloak session cookie existence - - If session was invalidated (not just expired), prevent auto-login - - Store logout reason in sessionStorage - -3. **Modify Auto-Login Logic**: - - Only auto-login if: - - User is truly unauthenticated (never logged in) - - AND no logout flags are set - - AND no session invalidation detected - - Don't auto-login if: - - Logout flags are set - - Session was invalidated - - User came from a logout flow - -4. **Handle Iframe Direct Logout**: - - Detect when Keycloak session is invalidated - - Set logout flags automatically - - Prevent auto-login - -## Current Code Issues - -### Issue 1: Auto-Login Logic (`app/signin/page.tsx:66-78`) -```typescript -if (status === "unauthenticated") { - // This triggers for ANY unauthenticated state - // Doesn't check if session was invalidated - signIn("keycloak", { callbackUrl: "/" }); -} -``` - -### Issue 2: Logout Detection (`app/signin/page.tsx:16-45`) -```typescript -// Only checks for explicit logout flags -// Doesn't detect session invalidation -const logoutParam = searchParams.get('logout'); -const fromLogout = sessionStorage.getItem('just_logged_out'); -``` - -### Issue 3: Session Invalidation Detection (`app/api/auth/options.ts:248-256`) -```typescript -// Detects session invalidation -// But doesn't set logout flags -// Sign-in page doesn't know session was invalidated -if (refreshedToken.error === "SessionNotActive") { - return { - ...refreshedToken, - accessToken: undefined, - // Should set a flag here to prevent auto-login - }; -} -``` - -## Summary - -**Why you're auto-logged in after iframe logout:** - -1. Iframe logs out directly (not via postMessage) -2. Keycloak session is cleared -3. Dashboard detects session invalidation -4. User becomes "unauthenticated" -5. Sign-in page auto-login logic triggers (after 1 second) -6. Keycloak still has SSO session cookie -7. User is auto-authenticated without credentials - -**The fix requires:** -- Detecting session invalidation and setting logout flags -- Preventing auto-login when session was invalidated -- Only auto-login for truly new users (never logged in) - diff --git a/IFRAME_LOGOUT_FIX.md b/IFRAME_LOGOUT_FIX.md deleted file mode 100644 index 049da544..00000000 --- a/IFRAME_LOGOUT_FIX.md +++ /dev/null @@ -1,89 +0,0 @@ -# Iframe Logout Session Invalidation Fix - -## Problem - -When a user logs out from an application inside an iframe: -1. The iframe application calls Keycloak logout endpoint -2. Keycloak session is invalidated -3. NextAuth dashboard still has a valid JWT token -4. When NextAuth tries to refresh the token, Keycloak returns: `{ error: 'invalid_grant', error_description: 'Session not active' }` -5. This causes a `JWT_SESSION_ERROR` and the user sees errors but isn't automatically signed out - -## Root Cause - -The `refreshAccessToken` function was catching all errors generically and setting `error: "RefreshAccessTokenError"`. When the session callback received this error, it would throw, causing a JWT_SESSION_ERROR but not properly signing the user out. - -## Solution - -### 1. Detect Session Invalidation - -In `refreshAccessToken`, we now specifically detect when Keycloak returns `invalid_grant` with "Session not active": - -```typescript -if (refreshedTokens.error === 'invalid_grant' || - refreshedTokens.error_description?.includes('Session not active') || - refreshedTokens.error_description?.includes('Token is not active')) { - return { - ...token, - error: "SessionNotActive", - }; -} -``` - -### 2. Clear Tokens in JWT Callback - -When we detect `SessionNotActive`, we clear the tokens in the JWT callback: - -```typescript -if (refreshedToken.error === "SessionNotActive") { - return { - ...refreshedToken, - accessToken: undefined, - refreshToken: undefined, - idToken: undefined, - }; -} -``` - -### 3. Return Null in Session Callback - -When tokens are missing or session is invalidated, the session callback returns `null`, which makes NextAuth treat the user as unauthenticated: - -```typescript -if (token.error === "SessionNotActive" || !token.accessToken) { - return null as any; // NextAuth will treat user as unauthenticated -} -``` - -## Result - -Now when a user logs out from an iframe application: -1. Keycloak session is invalidated -2. NextAuth detects the invalid session on next token refresh -3. Tokens are cleared -4. Session callback returns null -5. User is automatically treated as unauthenticated -6. NextAuth redirects to sign-in page (via AuthCheck component) - -## Files Modified - -- `app/api/auth/options.ts`: - - Enhanced `refreshAccessToken` to detect `invalid_grant` errors - - Clear tokens when session is invalidated - - Return null from session callback when session is invalid - -## Testing - -To test this fix: -1. Log in to the dashboard -2. Open an iframe application -3. Log out from the iframe application -4. Wait for NextAuth to try to refresh the token (or trigger a page refresh) -5. User should be automatically signed out and redirected to sign-in - ---- - -**Date**: 2024 -**Status**: ✅ Fixed -**Version**: 1.0 - diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md deleted file mode 100644 index 345950f9..00000000 --- a/IMPLEMENTATION_CHECKLIST.md +++ /dev/null @@ -1,286 +0,0 @@ -# Implementation Checklist: Unified Refresh System - -## 📋 Step-by-Step Implementation Guide - -### Phase 1: Foundation (Day 1) ⚡ CRITICAL - -#### ✅ Step 1.1: Create Refresh Manager -- [ ] Create `lib/services/refresh-manager.ts` -- [ ] Test singleton pattern -- [ ] Test register/unregister -- [ ] Test start/stop -- [ ] Test deduplication logic - -**Estimated Time**: 2-3 hours - ---- - -#### ✅ Step 1.2: Create Request Deduplication -- [ ] Create `lib/utils/request-deduplication.ts` -- [ ] Test deduplication with same key -- [ ] Test TTL expiration -- [ ] Test cleanup - -**Estimated Time**: 1 hour - ---- - -#### ✅ Step 1.3: Create Constants -- [ ] Create `lib/constants/refresh-intervals.ts` -- [ ] Define all intervals -- [ ] Export helper function - -**Estimated Time**: 30 minutes - ---- - -#### ✅ Step 1.4: Create Unified Hook -- [ ] Create `hooks/use-unified-refresh.ts` -- [ ] Test registration on mount -- [ ] Test cleanup on unmount -- [ ] Test manual refresh - -**Estimated Time**: 1-2 hours - ---- - -### Phase 2: Fix Critical Issues (Day 1-2) 🔴 URGENT - -#### ✅ Step 2.1: Fix Redis KEYS → SCAN -- [ ] Update `lib/services/notifications/notification-service.ts` line 293 -- [ ] Replace `redis.keys()` with `redis.scan()` -- [ ] Test with large key sets - -**Estimated Time**: 30 minutes - ---- - -#### ✅ Step 2.2: Fix Notification Hook Memory Leak -- [ ] Fix `hooks/use-notifications.ts` useEffect cleanup -- [ ] Remove function dependencies -- [ ] Test cleanup on unmount - -**Estimated Time**: 1 hour - ---- - -#### ✅ Step 2.3: Fix Notification Badge Double Fetch -- [ ] Update `components/notification-badge.tsx` -- [ ] Remove duplicate useEffect hooks -- [ ] Add request deduplication -- [ ] Test single fetch per action - -**Estimated Time**: 1 hour - ---- - -### Phase 3: Refactor Notifications (Day 2) 🟡 HIGH PRIORITY - -#### ✅ Step 3.1: Refactor useNotifications Hook -- [ ] Integrate unified refresh -- [ ] Add request deduplication -- [ ] Remove manual polling -- [ ] Test all functionality - -**Estimated Time**: 2-3 hours - ---- - -#### ✅ Step 3.2: Update Notification Badge -- [ ] Remove manual fetch logic -- [ ] Use hook's refresh function -- [ ] Test UI interactions - -**Estimated Time**: 1 hour - ---- - -### Phase 4: Refactor Widgets (Day 3-4) 🟢 MEDIUM PRIORITY - -#### ✅ Step 4.1: Refactor Calendar Widget -- [ ] Update `components/calendar.tsx` -- [ ] Use unified refresh hook -- [ ] Add request deduplication -- [ ] Test refresh functionality - -**Estimated Time**: 1 hour - ---- - -#### ✅ Step 4.2: Refactor Parole Widget -- [ ] Update `components/parole.tsx` -- [ ] Use unified refresh hook -- [ ] Remove manual interval -- [ ] Test chat updates - -**Estimated Time**: 1 hour - ---- - -#### ✅ Step 4.3: Refactor News Widget -- [ ] Update `components/news.tsx` -- [ ] Use unified refresh hook -- [ ] Add auto-refresh (was manual only) -- [ ] Test news updates - -**Estimated Time**: 1 hour - ---- - -#### ✅ Step 4.4: Refactor Email Widget -- [ ] Update `components/email.tsx` -- [ ] Use unified refresh hook -- [ ] Add auto-refresh (was manual only) -- [ ] Test email updates - -**Estimated Time**: 1 hour - ---- - -#### ✅ Step 4.5: Refactor Duties Widget -- [ ] Update `components/flow.tsx` -- [ ] Use unified refresh hook -- [ ] Add auto-refresh (was manual only) -- [ ] Test task updates - -**Estimated Time**: 1 hour - ---- - -#### ✅ Step 4.6: Refactor Navigation Bar Time -- [ ] Create `components/main-nav-time.tsx` -- [ ] Update `components/main-nav.tsx` to use new component -- [ ] Use unified refresh hook (1 second interval) -- [ ] Test time updates correctly -- [ ] Verify cleanup on unmount - -**Estimated Time**: 30 minutes - ---- - -### Phase 5: Testing & Validation (Day 5) ✅ FINAL - -#### ✅ Step 5.1: Memory Leak Testing -- [ ] Open DevTools Memory tab -- [ ] Monitor memory over 10 minutes -- [ ] Verify no memory leaks -- [ ] Check interval cleanup - -**Estimated Time**: 1 hour - ---- - -#### ✅ Step 5.2: API Call Reduction Testing -- [ ] Open DevTools Network tab -- [ ] Monitor API calls for 5 minutes -- [ ] Verify deduplication works -- [ ] Count total calls (should be ~60% less) - -**Estimated Time**: 1 hour - ---- - -#### ✅ Step 5.3: Performance Testing -- [ ] Test page load time -- [ ] Test widget refresh times -- [ ] Test with multiple tabs open -- [ ] Verify no performance degradation - -**Estimated Time**: 1 hour - ---- - -#### ✅ Step 5.4: User Experience Testing -- [ ] Test all widgets refresh correctly -- [ ] Test manual refresh buttons -- [ ] Test notification updates -- [ ] Verify smooth UX - -**Estimated Time**: 1 hour - ---- - -## 🎯 Daily Progress Tracking - -### Day 1 Target: -- [x] Phase 1: Foundation (Steps 1.1-1.4) -- [x] Phase 2: Critical Fixes (Steps 2.1-2.3) - -**Status**: ⏳ In Progress - ---- - -### Day 2 Target: -- [ ] Phase 3: Notifications (Steps 3.1-3.2) - -**Status**: ⏸️ Pending - ---- - -### Day 3 Target: -- [ ] Phase 4: Widgets Part 1 (Steps 4.1-4.2) - -**Status**: ⏸️ Pending - ---- - -### Day 4 Target: -- [ ] Phase 4: Widgets Part 2 (Steps 4.3-4.5) - -**Status**: ⏸️ Pending - ---- - -### Day 5 Target: -- [ ] Phase 5: Testing (Steps 5.1-5.4) - -**Status**: ⏸️ Pending - ---- - -## 🐛 Known Issues to Watch For - -1. **Race Conditions**: Monitor for duplicate requests -2. **Memory Leaks**: Watch for uncleaned intervals -3. **Performance**: Monitor API call frequency -4. **User Experience**: Ensure smooth refresh transitions - ---- - -## 📊 Success Criteria - -### Must Have: -- ✅ No memory leaks -- ✅ 60%+ reduction in API calls -- ✅ All widgets refresh correctly -- ✅ No duplicate requests - -### Nice to Have: -- ✅ Configurable refresh intervals -- ✅ Pause/resume functionality -- ✅ Refresh status monitoring -- ✅ Error recovery - ---- - -## 🔄 Rollback Plan - -If issues arise: - -1. **Keep old code**: Don't delete old implementations immediately -2. **Feature flag**: Use environment variable to toggle new/old system -3. **Gradual migration**: Migrate one widget at a time -4. **Monitor**: Watch for errors in production - ---- - -## 📝 Notes - -- All new code should be backward compatible -- Test each phase before moving to next -- Document any deviations from plan -- Update this checklist as you progress - ---- - -*Last Updated: Implementation Checklist v1.0* diff --git a/IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md b/IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md deleted file mode 100644 index c0c1f4a6..00000000 --- a/IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md +++ /dev/null @@ -1,888 +0,0 @@ -# Implementation Plan: Unified Refresh System - -## 🎯 Goals - -1. **Harmonize auto-refresh** across all widgets and notifications -2. **Reduce redundancy** and eliminate duplicate API calls -3. **Improve API efficiency** with request deduplication and caching -4. **Prevent memory leaks** with proper cleanup mechanisms -5. **Centralize refresh logic** for easier maintenance - ---- - -## 📋 Current State Analysis - -### Current Refresh Intervals: -- **Notifications**: 60 seconds (polling) -- **Calendar**: 5 minutes (300000ms) -- **Parole (Chat)**: 30 seconds (30000ms) -- **Navbar Time**: Static (not refreshing - needs fix) -- **News**: Manual only -- **Email**: Manual only -- **Duties (Tasks)**: Manual only - -### Current Problems: -1. ❌ No coordination between widgets -2. ❌ Duplicate API calls from multiple components -3. ❌ Memory leaks from uncleaned intervals -4. ❌ No request deduplication -5. ❌ Inconsistent refresh patterns - ---- - -## 🏗️ Architecture: Unified Refresh System - -### Phase 1: Core Infrastructure - -#### 1.1 Create Unified Refresh Manager - -**File**: `lib/services/refresh-manager.ts` - -```typescript -/** - * Unified Refresh Manager - * Centralizes all refresh logic, prevents duplicates, manages intervals - */ - -export type RefreshableResource = - | 'notifications' - | 'notifications-count' - | 'calendar' - | 'news' - | 'email' - | 'parole' - | 'duties'; - -export interface RefreshConfig { - resource: RefreshableResource; - interval: number; // milliseconds - enabled: boolean; - priority: 'high' | 'medium' | 'low'; - onRefresh: () => Promise; -} - -class RefreshManager { - private intervals: Map = new Map(); - private configs: Map = new Map(); - private pendingRequests: Map> = new Map(); - private lastRefresh: Map = new Map(); - private isActive = false; - - /** - * Register a refreshable resource - */ - register(config: RefreshConfig): void { - this.configs.set(config.resource, config); - - if (config.enabled && this.isActive) { - this.startRefresh(config.resource); - } - } - - /** - * Unregister a resource - */ - unregister(resource: RefreshableResource): void { - this.stopRefresh(resource); - this.configs.delete(resource); - this.lastRefresh.delete(resource); - } - - /** - * Start all refresh intervals - */ - start(): void { - if (this.isActive) return; - - this.isActive = true; - - // Start all enabled resources - this.configs.forEach((config, resource) => { - if (config.enabled) { - this.startRefresh(resource); - } - }); - } - - /** - * Stop all refresh intervals - */ - stop(): void { - this.isActive = false; - - // Clear all intervals - this.intervals.forEach((interval) => { - clearInterval(interval); - }); - - this.intervals.clear(); - } - - /** - * Start refresh for a specific resource - */ - private startRefresh(resource: RefreshableResource): void { - // Stop existing interval if any - this.stopRefresh(resource); - - const config = this.configs.get(resource); - if (!config || !config.enabled) return; - - // Initial refresh - this.executeRefresh(resource); - - // Set up interval - const interval = setInterval(() => { - this.executeRefresh(resource); - }, config.interval); - - this.intervals.set(resource, interval); - } - - /** - * Stop refresh for a specific resource - */ - private stopRefresh(resource: RefreshableResource): void { - const interval = this.intervals.get(resource); - if (interval) { - clearInterval(interval); - this.intervals.delete(resource); - } - } - - /** - * Execute refresh with deduplication - */ - private async executeRefresh(resource: RefreshableResource): Promise { - const config = this.configs.get(resource); - if (!config) return; - - const requestKey = `${resource}-${Date.now()}`; - const now = Date.now(); - const lastRefreshTime = this.lastRefresh.get(resource) || 0; - - // Prevent too frequent refreshes (minimum 1 second between same resource) - if (now - lastRefreshTime < 1000) { - console.log(`[RefreshManager] Skipping ${resource} - too soon`); - return; - } - - // Check if there's already a pending request for this resource - const pendingKey = `${resource}-pending`; - if (this.pendingRequests.has(pendingKey)) { - console.log(`[RefreshManager] Deduplicating ${resource} request`); - return; - } - - // Create and track the request - const refreshPromise = config.onRefresh() - .then(() => { - this.lastRefresh.set(resource, Date.now()); - }) - .catch((error) => { - console.error(`[RefreshManager] Error refreshing ${resource}:`, error); - }) - .finally(() => { - this.pendingRequests.delete(pendingKey); - }); - - this.pendingRequests.set(pendingKey, refreshPromise); - - try { - await refreshPromise; - } catch (error) { - // Error already logged above - } - } - - /** - * Manually trigger refresh for a resource - */ - async refresh(resource: RefreshableResource, force = false): Promise { - const config = this.configs.get(resource); - if (!config) { - throw new Error(`Resource ${resource} not registered`); - } - - if (force) { - // Force refresh: clear last refresh time - this.lastRefresh.delete(resource); - } - - await this.executeRefresh(resource); - } - - /** - * Get refresh status - */ - getStatus(): { - active: boolean; - resources: Array<{ - resource: RefreshableResource; - enabled: boolean; - lastRefresh: number | null; - interval: number; - }>; - } { - const resources = Array.from(this.configs.entries()).map(([resource, config]) => ({ - resource, - enabled: config.enabled, - lastRefresh: this.lastRefresh.get(resource) || null, - interval: config.interval, - })); - - return { - active: this.isActive, - resources, - }; - } -} - -// Singleton instance -export const refreshManager = new RefreshManager(); -``` - ---- - -#### 1.2 Create Request Deduplication Utility - -**File**: `lib/utils/request-deduplication.ts` - -```typescript -/** - * Request Deduplication Utility - * Prevents duplicate API calls for the same resource - */ - -interface PendingRequest { - promise: Promise; - timestamp: number; -} - -class RequestDeduplicator { - private pendingRequests = new Map>(); - private readonly DEFAULT_TTL = 5000; // 5 seconds - - /** - * Execute a request with deduplication - */ - async execute( - key: string, - requestFn: () => Promise, - ttl: number = this.DEFAULT_TTL - ): Promise { - // Check if there's a pending request - const pending = this.pendingRequests.get(key); - - if (pending) { - const age = Date.now() - pending.timestamp; - - // If request is still fresh, reuse it - if (age < ttl) { - console.log(`[RequestDeduplicator] Reusing pending request: ${key}`); - return pending.promise; - } else { - // Request is stale, remove it - this.pendingRequests.delete(key); - } - } - - // Create new request - const promise = requestFn() - .finally(() => { - // Clean up after request completes - this.pendingRequests.delete(key); - }); - - this.pendingRequests.set(key, { - promise, - timestamp: Date.now(), - }); - - return promise; - } - - /** - * Cancel a pending request - */ - cancel(key: string): void { - this.pendingRequests.delete(key); - } - - /** - * Clear all pending requests - */ - clear(): void { - this.pendingRequests.clear(); - } - - /** - * Get pending requests count - */ - getPendingCount(): number { - return this.pendingRequests.size; - } -} - -export const requestDeduplicator = new RequestDeduplicator(); -``` - ---- - -#### 1.3 Create Unified Refresh Hook - -**File**: `hooks/use-unified-refresh.ts` - -```typescript -/** - * Unified Refresh Hook - * Provides consistent refresh functionality for all widgets - */ - -import { useEffect, useCallback, useRef } from 'react'; -import { useSession } from 'next-auth/react'; -import { refreshManager, RefreshableResource } from '@/lib/services/refresh-manager'; - -interface UseUnifiedRefreshOptions { - resource: RefreshableResource; - interval: number; - enabled?: boolean; - onRefresh: () => Promise; - priority?: 'high' | 'medium' | 'low'; -} - -export function useUnifiedRefresh({ - resource, - interval, - enabled = true, - onRefresh, - priority = 'medium', -}: UseUnifiedRefreshOptions) { - const { status } = useSession(); - const onRefreshRef = useRef(onRefresh); - const isMountedRef = useRef(true); - - // Update callback ref when it changes - useEffect(() => { - onRefreshRef.current = onRefresh; - }, [onRefresh]); - - // Register/unregister with refresh manager - useEffect(() => { - if (status !== 'authenticated' || !enabled) { - return; - } - - isMountedRef.current = true; - - // Register with refresh manager - refreshManager.register({ - resource, - interval, - enabled: true, - priority, - onRefresh: async () => { - if (isMountedRef.current) { - await onRefreshRef.current(); - } - }, - }); - - // Start refresh manager if not already started - refreshManager.start(); - - // Cleanup - return () => { - isMountedRef.current = false; - refreshManager.unregister(resource); - }; - }, [resource, interval, enabled, priority, status]); - - // Manual refresh function - const refresh = useCallback( - async (force = false) => { - if (status !== 'authenticated') return; - await refreshManager.refresh(resource, force); - }, - [resource, status] - ); - - return { - refresh, - isActive: refreshManager.getStatus().active, - }; -} -``` - ---- - -### Phase 2: Harmonized Refresh Intervals - -#### 2.1 Define Standard Intervals - -**File**: `lib/constants/refresh-intervals.ts` - -```typescript -/** - * Standard Refresh Intervals - * All intervals in milliseconds - */ - -export const REFRESH_INTERVALS = { - // High priority - real-time updates - NOTIFICATIONS: 30000, // 30 seconds (was 60s) - NOTIFICATIONS_COUNT: 30000, // 30 seconds (same as notifications) - PAROLE: 30000, // 30 seconds (unchanged) - NAVBAR_TIME: 1000, // 1 second (navigation bar time - real-time) - - // Medium priority - frequent but not real-time - EMAIL: 60000, // 1 minute (was manual only) - DUTIES: 120000, // 2 minutes (was manual only) - - // Low priority - less frequent updates - CALENDAR: 300000, // 5 minutes (unchanged) - NEWS: 600000, // 10 minutes (was manual only) - - // Minimum interval between refreshes (prevents spam) - MIN_INTERVAL: 1000, // 1 second -} as const; - -/** - * Get refresh interval for a resource - */ -export function getRefreshInterval(resource: string): number { - switch (resource) { - case 'notifications': - return REFRESH_INTERVALS.NOTIFICATIONS; - case 'notifications-count': - return REFRESH_INTERVALS.NOTIFICATIONS_COUNT; - case 'parole': - return REFRESH_INTERVALS.PAROLE; - case 'email': - return REFRESH_INTERVALS.EMAIL; - case 'duties': - return REFRESH_INTERVALS.DUTIES; - case 'calendar': - return REFRESH_INTERVALS.CALENDAR; - case 'news': - return REFRESH_INTERVALS.NEWS; - default: - return 60000; // Default: 1 minute - } -} -``` - ---- - -### Phase 3: Refactor Widgets - -#### 3.1 Refactor Notification Hook - -**File**: `hooks/use-notifications.ts` (Refactored) - -```typescript -import { useState, useEffect, useCallback, useRef } from 'react'; -import { useSession } from 'next-auth/react'; -import { Notification, NotificationCount } from '@/lib/types/notification'; -import { useUnifiedRefresh } from './use-unified-refresh'; -import { REFRESH_INTERVALS } from '@/lib/constants/refresh-intervals'; -import { requestDeduplicator } from '@/lib/utils/request-deduplication'; - -const defaultNotificationCount: NotificationCount = { - total: 0, - unread: 0, - sources: {}, -}; - -export function useNotifications() { - const { data: session, status } = useSession(); - const [notifications, setNotifications] = useState([]); - const [notificationCount, setNotificationCount] = useState(defaultNotificationCount); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const isMountedRef = useRef(true); - - // Fetch notification count - const fetchNotificationCount = useCallback(async () => { - if (!session?.user || !isMountedRef.current) return; - - try { - setError(null); - - const data = await requestDeduplicator.execute( - `notifications-count-${session.user.id}`, - async () => { - const response = await fetch('/api/notifications/count', { - credentials: 'include', - }); - - if (!response.ok) { - throw new Error('Failed to fetch notification count'); - } - - return response.json(); - } - ); - - if (isMountedRef.current) { - setNotificationCount(data); - } - } catch (err) { - console.error('Error fetching notification count:', err); - if (isMountedRef.current) { - setError('Failed to fetch notification count'); - } - } - }, [session?.user]); - - // Fetch notifications - const fetchNotifications = useCallback(async (page = 1, limit = 20) => { - if (!session?.user || !isMountedRef.current) return; - - setLoading(true); - setError(null); - - try { - const data = await requestDeduplicator.execute( - `notifications-${session.user.id}-${page}-${limit}`, - async () => { - const response = await fetch(`/api/notifications?page=${page}&limit=${limit}`, { - credentials: 'include', - }); - - if (!response.ok) { - throw new Error('Failed to fetch notifications'); - } - - return response.json(); - } - ); - - if (isMountedRef.current) { - setNotifications(data.notifications); - } - } catch (err) { - console.error('Error fetching notifications:', err); - if (isMountedRef.current) { - setError('Failed to fetch notifications'); - } - } finally { - if (isMountedRef.current) { - setLoading(false); - } - } - }, [session?.user]); - - // Use unified refresh for notification count - useUnifiedRefresh({ - resource: 'notifications-count', - interval: REFRESH_INTERVALS.NOTIFICATIONS_COUNT, - enabled: status === 'authenticated', - onRefresh: fetchNotificationCount, - priority: 'high', - }); - - // Initial fetch - useEffect(() => { - isMountedRef.current = true; - - if (status === 'authenticated' && session?.user) { - fetchNotificationCount(); - fetchNotifications(); - } - - return () => { - isMountedRef.current = false; - }; - }, [status, session?.user, fetchNotificationCount, fetchNotifications]); - - // Mark as read - const markAsRead = useCallback(async (notificationId: string) => { - if (!session?.user) return false; - - try { - const response = await fetch(`/api/notifications/${notificationId}/read`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - }); - - if (!response.ok) return false; - - setNotifications(prev => - prev.map(n => n.id === notificationId ? { ...n, isRead: true } : n) - ); - - await fetchNotificationCount(); - return true; - } catch (err) { - console.error('Error marking notification as read:', err); - return false; - } - }, [session?.user, fetchNotificationCount]); - - // Mark all as read - const markAllAsRead = useCallback(async () => { - if (!session?.user) return false; - - try { - const response = await fetch('/api/notifications/read-all', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - }); - - if (!response.ok) return false; - - setNotifications(prev => prev.map(n => ({ ...n, isRead: true }))); - await fetchNotificationCount(); - return true; - } catch (err) { - console.error('Error marking all notifications as read:', err); - return false; - } - }, [session?.user, fetchNotificationCount]); - - return { - notifications, - notificationCount, - loading, - error, - fetchNotifications, - fetchNotificationCount, - markAsRead, - markAllAsRead, - }; -} -``` - ---- - -#### 3.2 Refactor Widget Components - -**Example: Calendar Widget** - -**File**: `components/calendar.tsx` (Refactored) - -```typescript -"use client"; - -import { useEffect, useState } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { RefreshCw, Calendar as CalendarIcon } from "lucide-react"; -import { useUnifiedRefresh } from '@/hooks/use-unified-refresh'; -import { REFRESH_INTERVALS } from '@/lib/constants/refresh-intervals'; -import { requestDeduplicator } from '@/lib/utils/request-deduplication'; -import { useSession } from 'next-auth/react'; - -interface Event { - id: string; - title: string; - start: string; - end: string; - allDay: boolean; - calendar: string; - calendarColor: string; -} - -export function Calendar() { - const { status } = useSession(); - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchEvents = async () => { - if (status !== 'authenticated') return; - - setLoading(true); - setError(null); - - try { - const calendarsData = await requestDeduplicator.execute( - 'calendar-events', - async () => { - const response = await fetch('/api/calendars?refresh=true'); - if (!response.ok) { - throw new Error('Failed to fetch events'); - } - return response.json(); - } - ); - - const now = new Date(); - now.setHours(0, 0, 0, 0); - - const allEvents = calendarsData.flatMap((calendar: any) => - (calendar.events || []).map((event: any) => ({ - id: event.id, - title: event.title, - start: event.start, - end: event.end, - allDay: event.isAllDay, - calendar: calendar.name, - calendarColor: calendar.color - })) - ); - - const upcomingEvents = allEvents - .filter((event: any) => new Date(event.start) >= now) - .sort((a: any, b: any) => new Date(a.start).getTime() - new Date(b.start).getTime()) - .slice(0, 7); - - setEvents(upcomingEvents); - } catch (err) { - console.error('Error fetching events:', err); - setError('Failed to load events'); - } finally { - setLoading(false); - } - }; - - // Use unified refresh - const { refresh } = useUnifiedRefresh({ - resource: 'calendar', - interval: REFRESH_INTERVALS.CALENDAR, - enabled: status === 'authenticated', - onRefresh: fetchEvents, - priority: 'low', - }); - - // Initial fetch - useEffect(() => { - if (status === 'authenticated') { - fetchEvents(); - } - }, [status]); - - // ... rest of component (formatDate, formatTime, render) - - return ( - - - Agenda - - - {/* ... */} - - ); -} -``` - ---- - -### Phase 4: Implementation Steps - -#### Step 1: Create Core Infrastructure (Day 1) - -1. ✅ Create `lib/services/refresh-manager.ts` -2. ✅ Create `lib/utils/request-deduplication.ts` -3. ✅ Create `lib/constants/refresh-intervals.ts` -4. ✅ Create `hooks/use-unified-refresh.ts` - -**Testing**: Unit tests for each module - ---- - -#### Step 2: Fix Memory Leaks (Day 1-2) - -1. ✅ Fix `useNotifications` hook cleanup -2. ✅ Fix notification badge double fetching -3. ✅ Fix widget interval cleanup -4. ✅ Fix Redis KEYS → SCAN - -**Testing**: Memory leak detection in DevTools - ---- - -#### Step 3: Refactor Notifications (Day 2) - -1. ✅ Refactor `hooks/use-notifications.ts` -2. ✅ Update `components/notification-badge.tsx` -3. ✅ Remove duplicate fetch logic - -**Testing**: Verify no duplicate API calls - ---- - -#### Step 4: Refactor Widgets (Day 3-4) - -1. ✅ Refactor `components/calendar.tsx` -2. ✅ Refactor `components/parole.tsx` -3. ✅ Refactor `components/news.tsx` -4. ✅ Refactor `components/email.tsx` -5. ✅ Refactor `components/flow.tsx` (Duties) -6. ✅ Refactor `components/main-nav.tsx` (Time display) - -**Testing**: Verify all widgets refresh correctly - ---- - -#### Step 5: Testing & Optimization (Day 5) - -1. ✅ Performance testing -2. ✅ Memory leak verification -3. ✅ API call reduction verification -4. ✅ User experience testing - ---- - -## 📊 Expected Improvements - -### Before: -- **API Calls**: ~120-150 calls/minute (with duplicates) -- **Memory Leaks**: Yes (intervals not cleaned up) -- **Refresh Coordination**: None -- **Request Deduplication**: None - -### After: -- **API Calls**: ~40-50 calls/minute (60-70% reduction) -- **Memory Leaks**: None (proper cleanup) -- **Refresh Coordination**: Centralized -- **Request Deduplication**: Full coverage - ---- - -## 🎯 Success Metrics - -1. **API Call Reduction**: 60%+ reduction in duplicate calls -2. **Memory Usage**: No memory leaks detected -3. **Performance**: Faster page loads, smoother UX -4. **Maintainability**: Single source of truth for refresh logic - ---- - -## 🚀 Quick Start Implementation - -### Priority Order: - -1. **Critical** (Do First): - - Fix memory leaks - - Create refresh manager - - Create request deduplication - -2. **High** (Do Second): - - Refactor notifications - - Refactor high-frequency widgets (parole, notifications) - -3. **Medium** (Do Third): - - Refactor medium-frequency widgets (email, duties) - -4. **Low** (Do Last): - - Refactor low-frequency widgets (calendar, news) - ---- - -## 📝 Notes - -- All intervals are configurable via constants -- Refresh manager can be paused/resumed globally -- Request deduplication prevents duplicate calls within 5 seconds -- All cleanup is handled automatically -- Compatible with existing code (gradual migration) - ---- - -*Implementation Plan v1.0* diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 8058f5f6..00000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,142 +0,0 @@ -# Implementation Summary - Logging Improvements - -**Date**: 2026-01-01 -**Status**: ✅ Completed - ---- - -## 📋 What Was Done - -### 1. Impact Analysis Completed ✅ - -**File Created**: `SESSION_CALLBACK_LOGGING_IMPACT_ANALYSIS.md` - -**Key Findings**: -- ✅ **No functional impact** on Keycloak, MinIO, or external services -- ✅ **Logging was added for debugging**, not functionality -- ✅ **Error logging will be preserved** (critical for troubleshooting) -- ✅ **Conditional logging recommended** (DEBUG_SESSION flag) - -**Recommendation**: -- Proceed with conditional logging using `DEBUG_SESSION` environment variable -- Keep error logging always enabled -- Make success logging conditional - ---- - -### 2. Mark-as-Read Logging Added ✅ - -**Files Modified**: -1. `app/api/notifications/[id]/read/route.ts` -2. `app/api/notifications/read-all/route.ts` - -**Logging Added**: -- ✅ Entry logging (when endpoint is called) -- ✅ Authentication status logging -- ✅ User ID and notification ID logging -- ✅ Success/failure logging with duration -- ✅ Error logging with stack traces -- ✅ Timestamp logging - -**Log Format**: -``` -[NOTIFICATION_API] Mark as read endpoint called -[NOTIFICATION_API] Mark as read - Processing { userId, notificationId, timestamp } -[NOTIFICATION_API] Mark as read - Success { userId, notificationId, duration } -``` - ---- - -## 🔍 What to Look For in Logs - -### When Mark-as-Read is Called - -**Expected Logs**: -``` -[NOTIFICATION_API] Mark as read endpoint called -[NOTIFICATION_API] Mark as read - Processing { userId: "...", notificationId: "...", timestamp: "..." } -[NOTIFICATION_API] Mark as read - Success { userId: "...", notificationId: "...", duration: "Xms" } -``` - -**If Authentication Fails**: -``` -[NOTIFICATION_API] Mark as read - Authentication failed -``` - -**If Operation Fails**: -``` -[NOTIFICATION_API] Mark as read - Failed { userId: "...", notificationId: "...", duration: "Xms" } -``` - -**If Error Occurs**: -``` -[NOTIFICATION_API] Mark as read - Error { error: "...", stack: "...", duration: "Xms" } -``` - ---- - -## 📊 Next Steps - -### Immediate (Ready to Test) - -1. **Test Mark-as-Read Functionality** - - Mark a single notification as read - - Mark all notifications as read - - Check logs for the new logging statements - - Verify notification count updates correctly - -2. **Monitor Logs** - - Watch for `[NOTIFICATION_API]` log entries - - Verify timing information - - Check for any errors - -### Future (When Ready) - -3. **Implement Conditional Session Callback Logging** - - Add `DEBUG_SESSION` environment variable support - - Update `app/api/auth/options.ts` - - Test in development and production - - Document in README - ---- - -## 🎯 Testing Checklist - -- [ ] Mark single notification as read → Check logs -- [ ] Mark all notifications as read → Check logs -- [ ] Verify notification count updates -- [ ] Check for any errors in logs -- [ ] Verify performance (duration logging) -- [ ] Test with invalid notification ID -- [ ] Test without authentication - ---- - -## 📝 Files Changed - -1. ✅ `app/api/notifications/[id]/read/route.ts` - Added comprehensive logging -2. ✅ `app/api/notifications/read-all/route.ts` - Added comprehensive logging -3. ✅ `SESSION_CALLBACK_LOGGING_IMPACT_ANALYSIS.md` - Created impact analysis -4. ✅ `IMPLEMENTATION_SUMMARY.md` - This file - ---- - -## 🔧 Environment Variables - -**No new environment variables required** for mark-as-read logging. - -**Future**: `DEBUG_SESSION` will be needed for conditional session callback logging (not implemented yet). - ---- - -## ✅ Status - -**Mark-as-Read Logging**: ✅ **COMPLETE** -**Session Callback Impact Analysis**: ✅ **COMPLETE** -**Session Callback Conditional Logging**: ⏳ **PENDING** (awaiting approval) - ---- - -**Generated**: 2026-01-01 -**Ready for Testing**: ✅ Yes - diff --git a/IMPROVEMENTS_LOGIN_FLOW.md b/IMPROVEMENTS_LOGIN_FLOW.md deleted file mode 100644 index 24d8d0b7..00000000 --- a/IMPROVEMENTS_LOGIN_FLOW.md +++ /dev/null @@ -1,684 +0,0 @@ -# Améliorations du Flow de Connexion - Recommandations - -## 📋 Vue d'ensemble - -Ce document propose des améliorations concrètes pour corriger et optimiser le flow de connexion/déconnexion du dashboard Next.js avec NextAuth et Keycloak. - ---- - -## 🎯 Problèmes Identifiés et Solutions - -### Problème 1 : `prompt=login` toujours actif - Empêche SSO naturel - -**Situation actuelle** : -```typescript -// app/api/auth/options.ts ligne 154 -authorization: { - params: { - scope: "openid profile email roles", - prompt: "login" // ⚠️ TOUJOURS actif - } -} -``` - -**Impact** : -- ❌ L'utilisateur doit **toujours** saisir ses identifiants, même lors de la première visite -- ❌ Empêche l'expérience SSO naturelle -- ❌ Mauvaise UX pour les utilisateurs légitimes - -**Solution recommandée** : Gérer `prompt=login` conditionnellement - -```typescript -// app/api/auth/options.ts -authorization: { - params: { - scope: "openid profile email roles", - // Ne pas forcer prompt=login par défaut - // prompt: "login" // ❌ À SUPPRIMER - } -} -``` - -**ET** : Ajouter `prompt=login` uniquement après un logout explicite - -```typescript -// Dans signIn() après logout -signIn("keycloak", { - callbackUrl: "/", - // Ajouter prompt=login uniquement si logout récent - ...(shouldForceLogin ? { prompt: "login" } : {}) -}); -``` - ---- - -### Problème 2 : Détection session invalide trop complexe et fragile - -**Situation actuelle** : -- Logique complexe dans `app/signin/page.tsx` (lignes 17-67) -- Vérification multiple de cookies, sessionStorage, URL params -- Race conditions possibles -- Auto-login peut se déclencher incorrectement - -**Solution recommandée** : Simplifier avec un flag serveur - -#### Option A : Utiliser un cookie serveur pour marquer le logout - -```typescript -// app/api/auth/end-sso-session/route.ts -export async function POST(request: NextRequest) { - // ... code existant ... - - // Après logout réussi, créer un cookie de flag - const response = NextResponse.json({ success: true }); - response.cookies.set('force_login_prompt', 'true', { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/', - maxAge: 300 // 5 minutes - }); - - return response; -} -``` - -#### Option B : Utiliser un paramètre d'état dans l'URL Keycloak - -```typescript -// Dans signout-handler.tsx, ajouter un paramètre state -const keycloakLogoutUrl = new URL( - `${keycloakIssuer}/protocol/openid-connect/logout` -); -keycloakLogoutUrl.searchParams.append('state', 'force_login'); -// Keycloak renverra ce state dans le redirect -``` - -**Simplification de signin/page.tsx** : - -```typescript -// Vérifier le cookie serveur au lieu de logique complexe -const forceLoginCookie = document.cookie - .split(';') - .find(c => c.trim().startsWith('force_login_prompt=')); - -if (forceLoginCookie) { - // Supprimer le cookie - document.cookie = 'force_login_prompt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - // Forcer prompt=login - signIn("keycloak", { - callbackUrl: "/", - // Ajouter prompt via custom params si possible - }); -} -``` - ---- - -### Problème 3 : Cookies Keycloak non supprimables (domaine différent) - -**Situation actuelle** : -- `clearKeycloakCookies()` échoue si Keycloak est sur un domaine différent -- Les cookies Keycloak persistent après logout -- Session SSO peut persister - -**Solutions recommandées** : - -#### Solution 1 : Utiliser Keycloak logout endpoint avec tous les paramètres - -```typescript -// Améliorer le logout URL dans signout-handler.tsx -const keycloakLogoutUrl = new URL( - `${keycloakIssuer}/protocol/openid-connect/logout` -); - -// Paramètres essentiels -keycloakLogoutUrl.searchParams.append('post_logout_redirect_uri', - window.location.origin + '/signin?logout=true'); -keycloakLogoutUrl.searchParams.append('id_token_hint', idToken); - -// ✅ AJOUTER ces paramètres pour forcer la suppression SSO -keycloakLogoutUrl.searchParams.append('kc_action', 'LOGOUT'); // Déjà présent -keycloakLogoutUrl.searchParams.append('logout_hint', 'true'); // Nouveau - -// Si possible, utiliser le client_id pour forcer logout client -if (process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID) { - keycloakLogoutUrl.searchParams.append('client_id', - process.env.NEXT_PUBLIC_KEYcloak_CLIENT_ID); -} -``` - -#### Solution 2 : Utiliser Admin API pour terminer TOUTES les sessions - -**Améliorer** `app/api/auth/end-sso-session/route.ts` : - -```typescript -// Au lieu de logout({ id: userId }), utiliser logoutAllSessions -try { - // Option 1 : Logout toutes les sessions de l'utilisateur - await adminClient.users.logout({ id: userId }); - - // Option 2 : Si disponible, utiliser une méthode plus agressive - // Note: Vérifier la version de Keycloak Admin Client - // Certaines versions supportent logoutAllSessions - - // Option 3 : Invalider les refresh tokens - const userSessions = await adminClient.users.listSessions({ id: userId }); - for (const session of userSessions) { - await adminClient.users.logoutSession({ - id: userId, - sessionId: session.id - }); - } -} catch (error) { - // ... gestion erreur -} -``` - -#### Solution 3 : Configurer Keycloak pour SameSite=None (si cross-domain) - -**Configuration Keycloak** (à faire côté Keycloak) : -``` -Cookie SameSite: None -Cookie Secure: true -Cookie Domain: .example.com (domaine parent partagé) -``` - -**Puis** améliorer `clearKeycloakCookies()` : - -```typescript -// lib/session.ts -export function clearKeycloakCookies() { - const keycloakIssuer = process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER; - if (!keycloakIssuer) return; - - try { - const keycloakUrl = new URL(keycloakIssuer); - const keycloakDomain = keycloakUrl.hostname; - - // Extraire le domaine parent si possible - const domainParts = keycloakDomain.split('.'); - const parentDomain = domainParts.length > 2 - ? '.' + domainParts.slice(-2).join('.') - : keycloakDomain; - - const keycloakCookieNames = [ - 'KEYCLOAK_SESSION', - 'KEYCLOAK_SESSION_LEGACY', - 'KEYCLOAK_IDENTITY', - 'KEYCLOAK_IDENTITY_LEGACY', - 'AUTH_SESSION_ID', - 'KC_RESTART', - 'KC_RESTART_LEGACY' - ]; - - // Essayer avec domaine parent (pour SameSite=None) - keycloakCookieNames.forEach(cookieName => { - // Avec domaine parent - document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${parentDomain}; SameSite=None; Secure;`; - // Sans domaine (same-origin) - document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=None; Secure;`; - // Avec path spécifique - document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/realms/; domain=${parentDomain}; SameSite=None; Secure;`; - }); - } catch (error) { - console.error('Error clearing Keycloak cookies:', error); - } -} -``` - ---- - -### Problème 4 : Race condition entre logout et auto-login - -**Situation actuelle** : -- Auto-login avec délai de 1 seconde -- Peut se déclencher pendant le flow de logout -- Flags `logout_in_progress` et `session_invalidated` peuvent être perdus - -**Solution recommandée** : Utiliser un mécanisme plus robuste - -#### Option A : Utiliser un cookie HttpOnly pour le flag - -```typescript -// Créer une route API pour marquer le logout -// app/api/auth/mark-logout/route.ts -export async function POST() { - const response = NextResponse.json({ success: true }); - response.cookies.set('logout_completed', 'true', { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/', - maxAge: 300 // 5 minutes - }); - return response; -} - -// Dans signout-handler.tsx -await fetch('/api/auth/mark-logout', { method: 'POST' }); - -// Dans signin/page.tsx -const logoutCompleted = document.cookie - .split(';') - .some(c => c.trim().startsWith('logout_completed=')); - -if (logoutCompleted) { - // Supprimer le cookie - document.cookie = 'logout_completed=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - // Ne pas auto-login - return; -} -``` - -#### Option B : Utiliser un état dans l'URL Keycloak callback - -```typescript -// Lors du logout, ajouter un paramètre state -const logoutState = btoa(JSON.stringify({ - logout: true, - timestamp: Date.now() -})); - -// Keycloak renverra ce state dans le redirect -// Dans signin/page.tsx, vérifier le state -const urlParams = new URLSearchParams(window.location.search); -const state = urlParams.get('state'); -if (state) { - try { - const stateData = JSON.parse(atob(state)); - if (stateData.logout) { - // Ne pas auto-login - return; - } - } catch (e) { - // State invalide, ignorer - } -} -``` - ---- - -### Problème 5 : Configuration cookies NextAuth non explicite - -**Situation actuelle** : -- Pas de configuration explicite des cookies NextAuth -- Utilise les valeurs par défaut -- Pas de contrôle sur SameSite, Secure, etc. - -**Solution recommandée** : Configurer explicitement les cookies - -```typescript -// app/api/auth/options.ts -export const authOptions: NextAuthOptions = { - // ... providers ... - - // ✅ AJOUTER configuration explicite des cookies - cookies: { - sessionToken: { - name: `next-auth.session-token`, - options: { - httpOnly: true, - sameSite: 'lax', - path: '/', - secure: process.env.NEXTAUTH_URL?.startsWith('https://') ?? false, - // Domaine explicite si nécessaire - // domain: process.env.COOKIE_DOMAIN, - }, - }, - callbackUrl: { - name: `next-auth.callback-url`, - options: { - httpOnly: true, - sameSite: 'lax', - path: '/', - secure: process.env.NEXTAUTH_URL?.startsWith('https://') ?? false, - }, - }, - csrfToken: { - name: `next-auth.csrf-token`, - options: { - httpOnly: true, - sameSite: 'lax', - path: '/', - secure: process.env.NEXTAUTH_URL?.startsWith('https://') ?? false, - }, - }, - state: { - name: `next-auth.state`, - options: { - httpOnly: true, - sameSite: 'lax', - path: '/', - secure: process.env.NEXTAUTH_URL?.startsWith('https://') ?? false, - }, - }, - }, - - // ... reste de la config ... -}; -``` - -**Avantages** : -- Contrôle total sur les cookies -- Peut ajuster SameSite pour cross-domain si nécessaire -- Meilleure sécurité - ---- - -### Problème 6 : Détection session invalide côté client uniquement - -**Situation actuelle** : -- Détection session invalide uniquement côté client -- Vérification de cookies via `document.cookie` -- Peut être contourné ou mal interprété - -**Solution recommandée** : Détection serveur + client - -#### Créer une route API pour vérifier l'état de session - -```typescript -// app/api/auth/session-status/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth/next'; -import { authOptions } from '../options'; - -export async function GET(request: NextRequest) { - const session = await getServerSession(authOptions); - - // Vérifier si la session existe mais est invalide - const hasSessionCookie = request.cookies.has('next-auth.session-token') || - request.cookies.has('__Secure-next-auth.session-token') || - request.cookies.has('__Host-next-auth.session-token'); - - return NextResponse.json({ - hasSession: !!session, - hasSessionCookie, - isInvalid: hasSessionCookie && !session, // Cookie existe mais session invalide - shouldForceLogin: request.cookies.get('force_login_prompt')?.value === 'true', - }); -} -``` - -#### Utiliser cette route dans signin/page.tsx - -```typescript -// app/signin/page.tsx -useEffect(() => { - const checkSessionStatus = async () => { - const response = await fetch('/api/auth/session-status'); - const status = await response.json(); - - if (status.isInvalid) { - // Session invalidée, ne pas auto-login - sessionStorage.setItem('session_invalidated', 'true'); - return; - } - - if (status.shouldForceLogin) { - // Forcer prompt=login - signIn("keycloak", { - callbackUrl: "/", - // prompt: "login" via custom params si possible - }); - return; - } - - // Nouvel utilisateur, auto-login OK - if (status === "unauthenticated" && !status.hasSessionCookie) { - signIn("keycloak", { callbackUrl: "/" }); - } - }; - - checkSessionStatus(); -}, []); -``` - ---- - -### Problème 7 : Gestion d'erreur token refresh insuffisante - -**Situation actuelle** : -- `refreshAccessToken()` détecte `SessionNotActive` -- Retourne `error: "SessionNotActive"` -- Mais la gestion peut être améliorée - -**Solution recommandée** : Améliorer la gestion d'erreur - -```typescript -// app/api/auth/options.ts -async function refreshAccessToken(token: ExtendedJWT) { - try { - const response = await fetch(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`, { - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - client_id: process.env.KEYCLOAK_CLIENT_ID!, - client_secret: process.env.KEYCLOAK_CLIENT_SECRET!, - grant_type: "refresh_token", - refresh_token: token.refreshToken || '', - }), - method: "POST", - }); - - const refreshedTokens = await response.json(); - - if (!response.ok) { - // ✅ AMÉLIORATION : Détecter différents types d'erreurs - const errorType = refreshedTokens.error; - const errorDescription = refreshedTokens.error_description || ''; - - // Session invalide (logout depuis iframe ou Keycloak) - if (errorType === 'invalid_grant' || - errorDescription.includes('Session not active') || - errorDescription.includes('Token is not active') || - errorDescription.includes('Session expired')) { - console.log("Keycloak session invalidated, marking for removal"); - return { - ...token, - error: "SessionNotActive", - // ✅ Supprimer tous les tokens - accessToken: undefined, - refreshToken: undefined, - idToken: undefined, - }; - } - - // Refresh token expiré (inactivité prolongée) - if (errorType === 'invalid_grant' && - errorDescription.includes('Refresh token expired')) { - console.log("Refresh token expired, user needs to re-authenticate"); - return { - ...token, - error: "RefreshTokenExpired", - accessToken: undefined, - refreshToken: undefined, - idToken: undefined, - }; - } - - // Autre erreur - throw refreshedTokens; - } - - return { - ...token, - accessToken: refreshedTokens.access_token, - refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, - idToken: token.idToken, // Keycloak ne renvoie pas de nouvel ID token - accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000, - error: undefined, // ✅ Clear any previous errors - }; - } catch (error: any) { - console.error("Error refreshing access token:", error); - - // ✅ AMÉLIORATION : Gestion d'erreur plus robuste - if (error?.error === 'invalid_grant' || - error?.error_description?.includes('Session not active') || - error?.error_description?.includes('Token is not active')) { - return { - ...token, - error: "SessionNotActive", - accessToken: undefined, - refreshToken: undefined, - idToken: undefined, - }; - } - - return { - ...token, - error: "RefreshAccessTokenError", - accessToken: undefined, - refreshToken: undefined, - idToken: undefined, - }; - } -} -``` - ---- - -## 🚀 Plan d'Implémentation Recommandé - -### Phase 1 : Corrections Critiques (Priorité Haute) - -1. **Supprimer `prompt=login` par défaut** - - Fichier : `app/api/auth/options.ts` - - Impact : Améliore l'UX pour les utilisateurs légitimes - -2. **Améliorer la gestion d'erreur token refresh** - - Fichier : `app/api/auth/options.ts` - - Impact : Meilleure détection des sessions invalides - -3. **Configurer explicitement les cookies NextAuth** - - Fichier : `app/api/auth/options.ts` - - Impact : Meilleur contrôle et sécurité - -### Phase 2 : Améliorations Logout (Priorité Moyenne) - -4. **Créer route API pour marquer logout** - - Nouveau fichier : `app/api/auth/mark-logout/route.ts` - - Modifier : `components/auth/signout-handler.tsx` - - Modifier : `app/signin/page.tsx` - - Impact : Élimine les race conditions - -5. **Améliorer Keycloak logout URL** - - Modifier : `components/auth/signout-handler.tsx` - - Modifier : `components/main-nav.tsx` - - Modifier : `components/layout/layout-wrapper.tsx` - - Impact : Meilleure suppression des sessions SSO - -6. **Améliorer `clearKeycloakCookies()`** - - Modifier : `lib/session.ts` - - Impact : Meilleure tentative de suppression cookies cross-domain - -### Phase 3 : Optimisations (Priorité Basse) - -7. **Créer route API pour vérifier statut session** - - Nouveau fichier : `app/api/auth/session-status/route.ts` - - Modifier : `app/signin/page.tsx` - - Impact : Détection plus fiable - -8. **Simplifier logique signin/page.tsx** - - Modifier : `app/signin/page.tsx` - - Impact : Code plus maintenable - ---- - -## 📝 Checklist d'Implémentation - -### Étape 1 : Configuration NextAuth -- [ ] Supprimer `prompt: "login"` par défaut dans `options.ts` -- [ ] Ajouter configuration explicite des cookies -- [ ] Améliorer gestion d'erreur `refreshAccessToken()` - -### Étape 2 : Amélioration Logout -- [ ] Créer route `/api/auth/mark-logout` -- [ ] Modifier `signout-handler.tsx` pour utiliser la route -- [ ] Améliorer Keycloak logout URL avec tous les paramètres -- [ ] Améliorer `clearKeycloakCookies()` pour cross-domain - -### Étape 3 : Amélioration Signin -- [ ] Créer route `/api/auth/session-status` -- [ ] Simplifier logique dans `signin/page.tsx` -- [ ] Utiliser cookie serveur pour détecter logout -- [ ] Ajouter `prompt=login` conditionnel après logout - -### Étape 4 : Tests -- [ ] Tester login normal (première visite) -- [ ] Tester login après logout (doit demander credentials) -- [ ] Tester logout depuis dashboard -- [ ] Tester logout depuis iframe -- [ ] Tester expiration session -- [ ] Tester refresh token expiré - ---- - -## 🔧 Configuration Keycloak Recommandée - -### Paramètres Client OAuth - -1. **Valid Redirect URIs** : - ``` - http://localhost:3000/api/auth/callback/keycloak - https://your-domain.com/api/auth/callback/keycloak - ``` - -2. **Web Origins** : - ``` - http://localhost:3000 - https://your-domain.com - ``` - -3. **Post Logout Redirect URIs** : - ``` - http://localhost:3000/signin?logout=true - https://your-domain.com/signin?logout=true - ``` - -4. **Access Token Lifespan** : 5 minutes (recommandé) -5. **SSO Session Idle** : 30 minutes -6. **SSO Session Max** : 10 heures - -### Configuration Realm - -1. **Cookies** : - - SameSite: `None` (si cross-domain) - - Secure: `true` (si HTTPS) - - Domain: Domaine parent partagé (si cross-domain) - -2. **Session Management** : - - Enable SSO Session Idle: `true` - - SSO Session Idle Timeout: 30 minutes - - SSO Session Max Lifespan: 10 heures - ---- - -## 📊 Métriques de Succès - -Après implémentation, vérifier : - -1. ✅ **Login première visite** : SSO fonctionne (pas de prompt si session Keycloak existe) -2. ✅ **Login après logout** : Prompt credentials demandé -3. ✅ **Logout dashboard** : Toutes les sessions supprimées -4. ✅ **Logout iframe** : Dashboard se déconnecte automatiquement -5. ✅ **Expiration session** : Redirection vers signin propre -6. ✅ **Refresh token expiré** : Redirection vers signin avec message approprié - ---- - -## 🎯 Résumé des Améliorations - -| Problème | Solution | Priorité | Fichiers Impactés | -|----------|----------|----------|-------------------| -| `prompt=login` toujours actif | Gérer conditionnellement | Haute | `options.ts`, `signin/page.tsx` | -| Détection session invalide complexe | Cookie serveur + route API | Haute | `signin/page.tsx`, `end-sso-session/route.ts` | -| Cookies Keycloak non supprimables | Améliorer logout URL + Admin API | Moyenne | `signout-handler.tsx`, `session.ts` | -| Race condition logout/login | Cookie HttpOnly pour flag | Moyenne | `signout-handler.tsx`, `signin/page.tsx` | -| Config cookies non explicite | Configuration explicite | Haute | `options.ts` | -| Gestion erreur refresh | Améliorer détection erreurs | Haute | `options.ts` | - ---- - -**Document créé le** : $(date) -**Dernière mise à jour** : Recommandations d'amélioration du flow de connexion - diff --git a/INACTIVITY_AND_LOGOUT_ANALYSIS.md b/INACTIVITY_AND_LOGOUT_ANALYSIS.md deleted file mode 100644 index 7b73402b..00000000 --- a/INACTIVITY_AND_LOGOUT_ANALYSIS.md +++ /dev/null @@ -1,232 +0,0 @@ -# Inactivity Timeout and Logout Analysis - -## Issue 1: Dashboard Should Disconnect After 30 Minutes of Inactivity - -### Current State - -**Session Configuration** (`app/api/auth/options.ts:190`): -```typescript -session: { - strategy: "jwt", - maxAge: 30 * 24 * 60 * 60, // 30 days -} -``` - -**SessionProvider Configuration** (`components/providers.tsx`): -```typescript - - {children} - -``` - -### Problem Analysis - -1. **No Inactivity Detection**: - - NextAuth session is set to 30 days maximum - - No client-side inactivity timeout logic exists - - No activity tracking (mouse movements, clicks, keyboard input) - - SessionProvider doesn't have `refetchInterval` configured - -2. **How NextAuth Sessions Work**: - - NextAuth sessions are JWT-based (stateless) - - Session validity is checked on each request to `/api/auth/session` - - No automatic expiration based on inactivity - - Session expires only when `maxAge` is reached (30 days in your case) - -3. **What's Missing**: - - Client-side activity monitoring - - Automatic session invalidation after inactivity period - - Session refresh based on activity (not just time) - -### Root Cause - -**NextAuth doesn't track user activity** - it only tracks session age. The session will remain valid for 30 days regardless of whether the user is active or not. - -### Solution Requirements - -To implement 30-minute inactivity timeout, you need: - -1. **Client-Side Activity Tracking**: - - Monitor user activity (mouse, keyboard, clicks) - - Track last activity timestamp - - Store in `sessionStorage` or `localStorage` - -2. **Session Invalidation Logic**: - - Check inactivity period on each page interaction - - Call `signOut()` if inactivity exceeds 30 minutes - - Clear NextAuth session and Keycloak session - -3. **Activity Reset on User Actions**: - - Reset inactivity timer on any user interaction - - Update last activity timestamp - -4. **SessionProvider Configuration**: - - Optionally configure `refetchInterval` to check session periodically - - But this won't help with inactivity - it only refreshes the session - -### Implementation Approach - -The inactivity timeout must be implemented **client-side** because: -- NextAuth sessions are stateless (JWT) -- Server doesn't know about user activity -- Activity tracking requires browser events - -**Recommended Implementation**: -1. Create an `InactivityHandler` component -2. Monitor user activity events (mousemove, keydown, click, scroll) -3. Store last activity time in `sessionStorage` -4. Check inactivity every minute (or on page focus) -5. If inactivity > 30 minutes, trigger logout - ---- - -## Issue 2: Applications Outside Dashboard Still Connected After Logout - -### Current Implementation - -**Logout Flow** (`components/main-nav.tsx`, `components/auth/signout-handler.tsx`): -1. Clear NextAuth cookies -2. Clear Keycloak cookies (client-side attempt) -3. Call `/api/auth/end-sso-session` (NEW) - - Uses Keycloak Admin API: `adminClient.users.logout({ id: userId })` -4. Sign out from NextAuth -5. Redirect to Keycloak logout endpoint with `id_token_hint` - -### Problem Analysis - -**Why Applications Are Still Connected:** - -1. **Keycloak Admin API `users.logout()` Behavior**: - - The method `adminClient.users.logout({ id: userId })` **logs out the user from all client sessions** - - However, it may **NOT clear the SSO session cookie** (`KEYCLOAK_SESSION`) - - The SSO session cookie is what allows applications to auto-authenticate - -2. **SSO Session vs Client Sessions**: - - **Client Sessions**: Per OAuth client (dashboard, app1, app2, etc.) - - **SSO Session**: Realm-wide, shared across all clients - - `users.logout()` clears client sessions but may leave SSO session active - - Applications check for SSO session cookie, not client sessions - -3. **Cookie Domain/Path Issues**: - - Keycloak cookies are set on Keycloak's domain - - Client-side `clearKeycloakCookies()` may not work if: - - Cookies are `HttpOnly` (can't be cleared from JavaScript) - - Cookies are on different domain (cross-domain restrictions) - - Cookies have different path/domain settings - -4. **Logout Endpoint Behavior**: - - Keycloak logout endpoint (`/protocol/openid-connect/logout`) with `id_token_hint`: - - Clears the **client session** for that specific OAuth client - - May clear SSO session **only if it's the last client session** - - If other applications have active sessions, SSO session persists - -### Root Cause - -**The SSO session cookie persists** because: -1. `users.logout()` Admin API method clears client sessions but may not clear SSO session cookie -2. Keycloak logout endpoint only clears SSO session if it's the last client session -3. If other applications have active sessions, the SSO session remains valid -4. Applications check for SSO session cookie, not client sessions - -### Why This Happens - -**Keycloak's SSO Design**: -- SSO session is designed to persist across client logouts -- This allows users to stay logged in across multiple applications -- Logging out from one application shouldn't log out from all applications -- This is **by design** for SSO functionality - -**However**, when you want **global logout**, you need to: -1. Clear the SSO session cookie explicitly -2. Or ensure all client sessions are logged out first -3. Or use Keycloak's Single Logout (SLO) feature - -### Solution Requirements - -To ensure applications are logged out: - -1. **Keycloak Configuration** (Server-Side): - - Enable **Front-Channel Logout** for all clients - - Configure **Back-Channel Logout URLs** for each client - - This allows Keycloak to notify all applications when logout occurs - -2. **Admin API Limitations**: - - `users.logout()` may not clear SSO session cookie - - Need to use Keycloak's logout endpoint with proper parameters - - Or use Keycloak Admin API to end SSO session directly (if available) - -3. **Alternative Approach**: - - Use Keycloak's **Single Logout (SLO)** feature - - Configure all clients to participate in SLO - - When one client logs out, all clients are notified - -### What's Actually Happening - -When you call `/api/auth/end-sso-session`: -1. ✅ Admin API `users.logout()` is called -2. ✅ All client sessions are logged out -3. ❌ SSO session cookie may still exist -4. ❌ Applications check SSO session cookie → still authenticated - -When you redirect to Keycloak logout endpoint: -1. ✅ Dashboard client session is cleared -2. ✅ If it's the last client session, SSO session is cleared -3. ❌ If other applications have active sessions, SSO session persists -4. ❌ Applications can still authenticate using SSO session cookie - -### Verification Steps - -To verify why applications are still connected: - -1. **Check if Admin API call succeeds**: - - Look for console logs: "Successfully ended SSO session for user: {userId}" - - Check for errors in `/api/auth/end-sso-session` endpoint - -2. **Check Keycloak session cookies**: - - After logout, check browser cookies for: - - `KEYCLOAK_SESSION` - - `KEYCLOAK_SESSION_LEGACY` - - `KEYCLOAK_IDENTITY` - - If these cookies still exist, SSO session is still active - -3. **Check if other applications have active sessions**: - - If other applications are open in other tabs/windows - - They may have active client sessions - - This prevents SSO session from being cleared - -4. **Check Keycloak Admin Console**: - - Navigate to: Users → [User] → Sessions - - Check if sessions are actually cleared - - Verify SSO session status - -### Recommended Solutions - -**Option 1: Keycloak Configuration (Recommended)** -- Enable Front-Channel Logout for all clients -- Configure Back-Channel Logout URLs -- This ensures all applications are notified of logout - -**Option 2: Clear SSO Session Cookie Explicitly** -- After Admin API logout, redirect to Keycloak logout endpoint -- Use `kc_action=LOGOUT` parameter (already implemented) -- Ensure all client sessions are logged out first - -**Option 3: Use Keycloak Single Logout (SLO)** -- Configure all clients to participate in SLO -- When dashboard logs out, all clients are automatically logged out -- Requires Keycloak configuration changes - ---- - -## Summary - -### Issue 1: 30-Minute Inactivity Timeout -- **Status**: Not implemented -- **Reason**: NextAuth doesn't track activity, only session age -- **Solution**: Client-side activity tracking + automatic logout - -### Issue 2: Applications Still Connected -- **Status**: Partially working -- **Reason**: SSO session cookie persists even after client sessions are cleared -- **Solution**: Keycloak configuration (Front-Channel Logout) or SLO - diff --git a/INVESTIGATION_502_ERROR.md b/INVESTIGATION_502_ERROR.md deleted file mode 100644 index 5f341c80..00000000 --- a/INVESTIGATION_502_ERROR.md +++ /dev/null @@ -1,224 +0,0 @@ -# Investigation Erreur 502 - Redirection depuis Keycloak - -## 🔍 Problème Identifié - -**Symptôme** : Erreur 502 après redirection depuis Keycloak après authentification - -**Logs observés** : -``` -Keycloak profile callback: { - rawProfile: { ... }, - rawRoles: undefined, - realmAccess: undefined, // ⚠️ PROBLÈME ICI - groups: [ ... ] // ✅ Groups présents -} -Profile callback raw roles: [] -Profile callback cleaned roles: [] -``` - -## 🎯 Cause Racine - -Le profil Keycloak (ID token) **ne contient pas** `realm_access.roles`, mais contient `groups`. Les rôles sont probablement dans le **token d'accès**, pas dans le token ID. - -## ✅ Correction Appliquée - -**Fichier** : `app/api/auth/options.ts` - Callback JWT - -**Changements** : -1. ✅ Extraction des rôles depuis le **token d'accès** (pas seulement le profil) -2. ✅ Fallback sur `groups` si pas de rôles -3. ✅ Logs améliorés pour debugging - -**Code modifié** : -```typescript -// Avant : Seulement depuis profile -const roles = keycloakProfile.realm_access?.roles || []; - -// Après : Multi-sources -1. Essayer depuis profile (ID token) -2. Si vide, décoder access token -3. Si toujours vide, utiliser groups comme fallback -``` - -## 🔍 Points d'Investigation pour l'Erreur 502 - -### 1. Vérifier les logs serveur Next.js complets - -**Où chercher** : -- Terminal Next.js (erreurs complètes) -- Logs de production (si déployé) -- Console navigateur (erreurs client) - -**Commandes utiles** : -```bash -# Voir tous les logs -npm run dev 2>&1 | tee logs.txt - -# Filtrer les erreurs -npm run dev 2>&1 | grep -i "error\|502\|exception" -``` - -### 2. Vérifier le callback NextAuth - -**Fichier** : `app/api/auth/callback/keycloak` (géré par NextAuth) - -**Points à vérifier** : -- ✅ Le callback reçoit bien le code OAuth -- ✅ L'échange code → tokens fonctionne -- ✅ Le callback JWT s'exécute sans erreur -- ✅ Le callback session s'exécute sans erreur - -**Ajouter des logs** : -```typescript -// Dans options.ts, callback jwt -console.log('JWT callback - account:', !!account, 'profile:', !!profile); -console.log('JWT callback - accessToken length:', account?.access_token?.length); -console.log('JWT callback - roles extracted:', cleanRoles); -``` - -### 3. Vérifier l'initialisation storage - -**Fichier** : `app/api/storage/init/route.ts` - -**Problème potentiel** : -- Si `createUserFolderStructure()` échoue, ça peut causer une 502 -- Si la session n'est pas encore complètement créée - -**Vérifications** : -```typescript -// Vérifier si l'erreur vient de là -console.log('Storage init - session:', session); -console.log('Storage init - user id:', session?.user?.id); -``` - -### 4. Vérifier la configuration Keycloak - -**Problème potentiel** : -- Les rôles ne sont pas mappés correctement dans Keycloak -- Le scope "roles" n'est pas configuré correctement -- Les mappers de token ne sont pas configurés - -**À vérifier dans Keycloak** : -1. **Client Configuration** : - - Scope "roles" est-il activé ? - - Mapper "realm roles" est-il configuré ? - -2. **Token Mappers** : - - `realm_access.roles` mapper existe-t-il ? - - Est-il ajouté au token d'accès ? - -3. **User Roles** : - - L'utilisateur a-t-il des rôles assignés dans le realm ? - -### 5. Vérifier les erreurs dans le callback session - -**Fichier** : `app/api/auth/options.ts` - Callback session - -**Problème potentiel** : -- Si `token.role` est undefined et qu'un code s'attend à un array -- Si `session.user` est mal formé - -**Vérifications** : -```typescript -// Dans callback session -console.log('Session callback - token.role:', token.role); -console.log('Session callback - token.error:', token.error); -console.log('Session callback - hasAccessToken:', !!token.accessToken); -``` - -### 6. Vérifier les timeouts - -**Problème potentiel** : -- Timeout lors de l'appel à Keycloak pour échanger le code -- Timeout lors de l'initialisation storage -- Timeout lors de la création de la session - -**Solutions** : -- Augmenter les timeouts si nécessaire -- Vérifier la latence réseau vers Keycloak - -## 🛠️ Actions Immédiates - -### Action 1 : Ajouter plus de logs - -**Fichier** : `app/api/auth/options.ts` - -```typescript -// Dans callback jwt, après extraction des rôles -console.log('=== JWT CALLBACK DEBUG ==='); -console.log('Has account:', !!account); -console.log('Has profile:', !!profile); -console.log('Access token present:', !!account?.access_token); -console.log('Roles from profile:', keycloakProfile.realm_access?.roles); -console.log('Roles from access token:', roles); -console.log('Final roles:', cleanRoles); -console.log('=========================='); -``` - -### Action 2 : Vérifier l'erreur exacte - -**Dans le terminal Next.js**, chercher : -- Stack trace complète -- Message d'erreur exact -- Ligne de code qui cause l'erreur - -### Action 3 : Tester avec un utilisateur simple - -**Tester avec** : -- Un utilisateur avec des rôles -- Un utilisateur sans rôles -- Vérifier si l'erreur est liée aux rôles - -### Action 4 : Vérifier la configuration Keycloak - -**Dans Keycloak Admin Console** : - -1. **Client → Mappers** : - - Vérifier qu'il y a un mapper "realm roles" - - Vérifier qu'il est ajouté au token d'accès - - Vérifier qu'il ajoute `realm_access.roles` - -2. **Client → Settings** : - - Vérifier que "Full Scope Allowed" est activé - - Vérifier les "Default Client Scopes" incluent "roles" - -3. **Realm → Roles** : - - Vérifier que l'utilisateur a des rôles assignés - -## 📊 Checklist de Debugging - -- [ ] Logs serveur Next.js complets vérifiés -- [ ] Erreur exacte identifiée (stack trace) -- [ ] Callback JWT s'exécute sans erreur -- [ ] Callback session s'exécute sans erreur -- [ ] Rôles extraits correctement (depuis access token) -- [ ] Storage init fonctionne -- [ ] Configuration Keycloak vérifiée -- [ ] Mappers Keycloak vérifiés -- [ ] Timeouts vérifiés - -## 🔧 Solution Temporaire (si nécessaire) - -Si l'erreur persiste, on peut temporairement utiliser les `groups` comme rôles : - -```typescript -// Dans profile callback -const roles = keycloakProfile.realm_access?.roles || - keycloakProfile.groups || - []; -``` - -**Note** : Ce n'est qu'une solution temporaire. Il faut corriger la configuration Keycloak pour avoir les rôles correctement mappés. - -## 📝 Prochaines Étapes - -1. ✅ **Correction appliquée** : Extraction rôles depuis access token -2. ⏳ **À faire** : Vérifier les logs serveur pour l'erreur exacte -3. ⏳ **À faire** : Vérifier configuration Keycloak -4. ⏳ **À faire** : Tester après correction - ---- - -**Document créé le** : $(date) -**Statut** : Correction appliquée, investigation en cours - diff --git a/KEYCLOAK_SESSION_SYNC.md b/KEYCLOAK_SESSION_SYNC.md deleted file mode 100644 index 64bfa32d..00000000 --- a/KEYCLOAK_SESSION_SYNC.md +++ /dev/null @@ -1,123 +0,0 @@ -# Keycloak Session Synchronization Fix - -## Problem - -When a user is still logged into the NextAuth dashboard (session valid for 30 days), but Keycloak session cookies have expired (typically 30 minutes to a few hours), iframe applications can't authenticate because they rely on Keycloak cookies for SSO. - -**Symptoms**: -- User is logged into dashboard -- Iframe applications ask for Keycloak login again -- NextAuth session is still valid, but Keycloak cookies expired - -## Root Cause - -**Session Mismatch**: -- **NextAuth Session**: 30 days (JWT-based, stored in encrypted cookie) -- **Keycloak Session Cookies**: Typically 30 minutes to a few hours (set by Keycloak server) -- **Iframe Applications**: Rely on Keycloak session cookies for SSO, not NextAuth tokens - -When Keycloak session cookies expire, iframe applications can't authenticate even though NextAuth session is still valid. - -## Solution Implemented - -### 1. Session Refresh API Endpoint - -Created `/api/auth/refresh-keycloak-session` that: -- Uses the refresh token to get new Keycloak tokens -- Ensures tokens are fresh before loading iframes -- Helps maintain token synchronization - -### 2. Automatic Session Refresh Before Iframe Load - -Updated `ResponsiveIframe` component to: -- Automatically refresh the session before loading iframe applications -- Show a loading indicator during refresh -- Ensure tokens are fresh when iframes load - -### 3. Exposed Refresh Token in Session - -- Added `refreshToken` to session object -- Allows API endpoints to refresh tokens when needed - -## Files Modified - -1. **`app/api/auth/refresh-keycloak-session/route.ts`** (NEW) - - API endpoint to refresh Keycloak tokens - - Uses refresh token to get new access tokens - -2. **`app/components/responsive-iframe.tsx`** - - Automatically refreshes session before loading iframe - - Shows loading indicator during refresh - -3. **`app/api/auth/options.ts`** - - Exposes `refreshToken` in session object - -4. **`types/next-auth.d.ts`** - - Added `refreshToken` to Session interface - -## Limitations - -**Important**: This solution refreshes OAuth tokens, but **Keycloak session cookies are separate** and are set by Keycloak when the user authenticates via the browser. Refreshing OAuth tokens doesn't automatically refresh Keycloak session cookies. - -### Why This Happens - -Keycloak maintains two separate sessions: -1. **OAuth Token Session**: Managed via refresh tokens (what we refresh) -2. **Browser Session Cookies**: Set by Keycloak during login, expire based on Keycloak's session timeout settings - -### Recommended Solutions - -#### Option 1: Configure Keycloak Session Timeout (Recommended) - -Increase Keycloak's SSO session timeout to match or exceed NextAuth's 30-day session: - -1. Go to Keycloak Admin Console -2. Navigate to: Realm Settings → Sessions -3. Set **SSO Session Idle** to match your needs (e.g., 30 days) -4. Set **SSO Session Max** to match (e.g., 30 days) - -This ensures Keycloak cookies don't expire before NextAuth session. - -#### Option 2: Pass Access Token to Iframe Applications - -If iframe applications support token-based authentication: -- Pass `accessToken` via URL parameter: `?token=${accessToken}` -- Or use `postMessage` to send token to iframe -- Iframe applications can then use the token for authentication - -#### Option 3: Periodic Session Refresh - -Implement a periodic refresh mechanism that: -- Checks session validity every 15-20 minutes -- Refreshes tokens proactively -- May help keep Keycloak session active - -## Testing - -1. Log in to dashboard -2. Wait for Keycloak session to expire (or manually clear Keycloak cookies) -3. Navigate to an iframe application -4. Session should be refreshed automatically -5. Iframe should load without requiring login - -## Environment Variables Required - -```bash -NEXT_PUBLIC_KEYCLOAK_ISSUER=https://keycloak.example.com/realms/neah -KEYCLOAK_CLIENT_ID=neah-dashboard -KEYCLOAK_CLIENT_SECRET= -``` - -## Future Improvements - -1. **Implement invisible iframe to Keycloak**: Use Keycloak's check-session-iframe to refresh cookies -2. **Token passing**: Pass access tokens to iframe applications if they support it -3. **Proactive refresh**: Implement periodic token refresh to prevent expiration -4. **Session monitoring**: Monitor Keycloak session status and refresh proactively - ---- - -**Date**: 2024 -**Status**: ✅ Implemented (with limitations) -**Version**: 1.0 - diff --git a/LEANTIME_API_FIXES.md b/LEANTIME_API_FIXES.md deleted file mode 100644 index 73cb379f..00000000 --- a/LEANTIME_API_FIXES.md +++ /dev/null @@ -1,157 +0,0 @@ -# Leantime API Fixes - Mark Notifications as Read - -**Date**: 2026-01-01 -**Issue**: Mark all as read failing due to incorrect API method names -**Status**: ✅ Fixed - ---- - -## 🔍 Issues Found - -### Issue 1: Incorrect Method Name for Single Notification - -**Current Code** (WRONG): -```typescript -method: 'leantime.rpc.Notifications.Notifications.markNotificationAsRead' -params: { - userId: leantimeUserId, - notificationId: parseInt(sourceId) // Wrong parameter name -} -``` - -**Leantime Documentation** (CORRECT): -```typescript -method: 'leantime.rpc.Notifications.Notifications.markNotificationRead' // No "As" in method name -params: { - id: parseInt(sourceId), // Parameter is "id", not "notificationId" - userId: leantimeUserId -} -``` - -**Fix Applied**: ✅ Changed method name and parameter names to match Leantime API - ---- - -### Issue 2: No "Mark All" Method Exists - -**Problem**: -- Leantime API does NOT have a `markAllNotificationsAsRead` method -- Current code tries to call a non-existent method - -**Solution**: -- Fetch all unread notifications -- Mark each one individually using `markNotificationRead` -- Process in parallel for better performance - -**Fix Applied**: ✅ Implemented loop-based approach to mark all notifications individually - ---- - -## ✅ Changes Made - -### 1. Fixed `markAsRead` Method - -**File**: `lib/services/notifications/leantime-adapter.ts` - -**Changes**: -- ✅ Method name: `markNotificationAsRead` → `markNotificationRead` -- ✅ Parameter: `notificationId` → `id` -- ✅ Parameter order: `id` first, then `userId` (matching Leantime docs) -- ✅ Added request logging - ---- - -### 2. Fixed `markAllAsRead` Method - -**File**: `lib/services/notifications/leantime-adapter.ts` - -**New Implementation**: -1. Fetch all unread notifications (up to 1000) -2. Filter to get only unread ones -3. Mark each notification individually using `markNotificationRead` -4. Process in parallel using `Promise.all()` -5. Return success if majority succeed - -**Benefits**: -- ✅ Works with actual Leantime API -- ✅ Handles partial failures gracefully -- ✅ Parallel processing for better performance -- ✅ Detailed logging for each notification - ---- - -## 📊 Expected Behavior After Fix - -### Mark Single Notification as Read - -**Before**: ❌ Failed (wrong method name) -**After**: ✅ Should work correctly - -**Logs**: -``` -[LEANTIME_ADAPTER] markAsRead - Request body: {"method":"markNotificationRead",...} -[LEANTIME_ADAPTER] markAsRead - Success: true -``` - ---- - -### Mark All Notifications as Read - -**Before**: ❌ Failed (method doesn't exist) -**After**: ✅ Should work (marks each individually) - -**Logs**: -``` -[LEANTIME_ADAPTER] markAllAsRead - Fetching all unread notifications -[LEANTIME_ADAPTER] markAllAsRead - Found 66 unread notifications to mark -[LEANTIME_ADAPTER] markAllAsRead - Results: 66 succeeded, 0 failed out of 66 total -[LEANTIME_ADAPTER] markAllAsRead - Overall success: true -``` - ---- - -## 🎯 Count vs Display Issue - -**Current Situation**: -- Count: 66 unread (from first 100 notifications) -- Display: 10 notifications shown (pagination) - -**Why**: -- `getNotificationCount()` fetches first 100 notifications and counts unread -- `getNotifications()` with default limit=20 shows first 10-20 -- This is expected behavior but can be confusing - -**Options**: -1. **Accept limitation**: Document that count is based on first 100 -2. **Fetch all for count**: More accurate but slower -3. **Use dedicated count API**: If Leantime provides one -4. **Show "66+ unread"**: If count reaches 100, indicate there may be more - -**Recommendation**: Keep current behavior but add a note in UI if count = 100 (may have more) - ---- - -## 🚀 Next Steps - -1. ✅ **Test Mark Single as Read**: Should work now with correct method name -2. ✅ **Test Mark All as Read**: Should work by marking each individually -3. ⏳ **Verify Count Updates**: After marking, count should decrease -4. ⏳ **Monitor Performance**: Marking 66 notifications individually may take a few seconds - ---- - -## 📝 Summary - -**Fixes Applied**: -1. ✅ Fixed `markAsRead` method name and parameters -2. ✅ Implemented `markAllAsRead` using individual marking approach -3. ✅ Added comprehensive logging - -**Status**: Ready for testing after `rm -rf .next && npm run build` - -**Expected Result**: Mark all as read should now work correctly - ---- - -**Generated**: 2026-01-01 - diff --git a/LOGIN_LOGOUT_FILES_AUDIT.md b/LOGIN_LOGOUT_FILES_AUDIT.md deleted file mode 100644 index 646ebdad..00000000 --- a/LOGIN_LOGOUT_FILES_AUDIT.md +++ /dev/null @@ -1,646 +0,0 @@ -# Audit Complet des Fichiers Login/Logout - Analyse des Cookies - -## 📋 Vue d'ensemble - -Ce document liste **TOUS** les fichiers de code impliqués dans le processus de **login** et **logout** du dashboard Next.js avec NextAuth et Keycloak, avec une analyse approfondie de la gestion des cookies. - ---- - -## 🔐 FICHIERS CORE - Configuration NextAuth - -### 1. **`app/api/auth/[...nextauth]/route.ts`** -**Rôle** : Route handler NextAuth pour tous les endpoints d'authentification -**Cookies gérés** : -- `next-auth.session-token` (ou variantes sécurisées) - Cookie principal de session NextAuth -- `next-auth.csrf-token` - Token CSRF pour la sécurité -- `next-auth.state` - État OAuth pour le flow Keycloak -- `next-auth.callback-url` - URL de callback après authentification - -**Fonctions** : -- Gère `GET/POST /api/auth/signin` → Redirige vers Keycloak -- Gère `GET/POST /api/auth/signout` → Déconnecte et nettoie les cookies -- Gère `GET /api/auth/session` → Lit le cookie de session -- Gère `GET /api/auth/callback/keycloak` → Reçoit le code OAuth de Keycloak -- Gère `GET /api/auth/csrf` → Génère le token CSRF -- Gère `GET /api/auth/providers` → Liste les providers disponibles - -**Cookies créés/supprimés** : -- **Login** : Crée `next-auth.session-token` (HttpOnly, Secure, SameSite=Lax) -- **Logout** : Supprime `next-auth.session-token` via `signOut()` - ---- - -### 2. **`app/api/auth/options.ts`** ⭐ **FICHIER CRITIQUE** -**Rôle** : Configuration principale de NextAuth avec Keycloak -**Cookies gérés** : -- Tous les cookies NextAuth (via configuration implicite) -- Les tokens Keycloak sont stockés dans le JWT (pas de cookies séparés) - -**Fonctions clés** : - -#### `refreshAccessToken(token)` (lignes 83-139) -- **Cookies utilisés** : Aucun directement, mais utilise `refreshToken` du JWT -- **Comportement** : - - Appelle Keycloak `/token` endpoint pour rafraîchir - - Détecte si la session Keycloak est invalide (erreur `invalid_grant`) - - Retourne `error: "SessionNotActive"` si session Keycloak expirée - -#### `jwt` callback (lignes 196-282) -- **Cookies utilisés** : Lit `next-auth.session-token` (décrypté par NextAuth) -- **Comportement** : - - **Initial login** : Stocke `accessToken`, `refreshToken`, `idToken` dans le JWT - - **Subsequent requests** : Vérifie expiration, rafraîchit si nécessaire - - **Token expired** : Appelle `refreshAccessToken()` - - **Session invalidated** : Retourne token avec `error: "SessionNotActive"` - -#### `session` callback (lignes 283-324) -- **Cookies utilisés** : Lit le JWT depuis `next-auth.session-token` -- **Comportement** : - - Si `token.error === "SessionNotActive"` → Retourne `null` (force logout) - - Sinon, construit la session avec les données utilisateur - -**Configuration cookies** : -```typescript -session: { - strategy: "jwt", - maxAge: 4 * 60 * 60, // 4 heures -} -// Les cookies sont gérés automatiquement par NextAuth -// Pas de configuration explicite des cookies dans ce fichier -``` - -**Paramètres OAuth** : -```typescript -authorization: { - params: { - scope: "openid profile email roles", - prompt: "login" // Force le prompt de login même si SSO existe - } -} -``` - ---- - -## 🚪 FICHIERS PAGES - Interface Utilisateur - -### 3. **`app/signin/page.tsx`** ⭐ **FICHIER CRITIQUE** -**Rôle** : Page de connexion avec logique complexe de détection de logout -**Cookies analysés** : -- `next-auth.session-token` (ou variantes) - Vérifie si cookie existe mais invalide -- `logout_in_progress` - Cookie temporaire (60s) pour marquer logout en cours -- Cookies Keycloak (via `document.cookie`) - -**Fonctions clés** : - -#### Détection de logout/session invalide (lignes 17-67) -```typescript -// Vérifie les cookies NextAuth -const hasInvalidSessionCookie = document.cookie - .split(';') - .some(c => c.trim().startsWith('next-auth.session-token=') || - c.trim().startsWith('__Secure-next-auth.session-token=') || - c.trim().startsWith('__Host-next-auth.session-token=')); - -// Si cookie existe mais status = unauthenticated → Session invalidée -if (status === 'unauthenticated' && hasInvalidSessionCookie) { - sessionStorage.setItem('session_invalidated', 'true'); - // Empêche auto-login -} -``` - -#### Auto-login (lignes 69-124) -- **Condition** : Seulement si **PAS** de cookie de session existant -- **Comportement** : Appelle `signIn("keycloak")` après 1 seconde -- **Protection** : Ne s'exécute pas si `logout_in_progress` ou `session_invalidated` - -#### Initialisation storage (lignes 126-158) -- Appelle `/api/storage/init` après authentification réussie -- Force reload pour mettre à jour la session - -**Cookies créés/supprimés** : -- **Aucun cookie créé directement** (NextAuth gère ça) -- **Supprime** : `sessionStorage` items (`just_logged_out`, `session_invalidated`) - ---- - -### 4. **`app/signout/page.tsx`** -**Rôle** : Page de déconnexion (simple wrapper) -**Cookies** : Aucune manipulation directe, délègue à `SignOutHandler` - ---- - -## 🔧 FICHIERS COMPOSANTS - Logique Métier - -### 5. **`components/auth/signout-handler.tsx`** ⭐ **FICHIER CRITIQUE** -**Rôle** : Gère la déconnexion complète (NextAuth + Keycloak) -**Cookies manipulés** : - -#### Cookies NextAuth (ligne 23) -```typescript -clearAuthCookies(); // Supprime next-auth.session-token -``` - -#### Cookies Keycloak (ligne 25) -```typescript -clearKeycloakCookies(); // Tente de supprimer KEYCLOAK_SESSION, etc. -``` - -#### Cookie de flag (ligne 16) -```typescript -document.cookie = 'logout_in_progress=true; path=/; max-age=60'; -``` - -**Flow de logout** : -1. Marque logout en cours (`sessionStorage` + cookie) -2. **Supprime cookies NextAuth** (`clearAuthCookies()`) -3. **Tente de supprimer cookies Keycloak** (`clearKeycloakCookies()`) -4. Appelle `/api/auth/end-sso-session` (Admin API Keycloak) -5. Appelle `signOut()` NextAuth (supprime cookie serveur) -6. Redirige vers Keycloak logout endpoint avec `id_token_hint` -7. Keycloak redirige vers `/signin?logout=true` - -**Cookies supprimés** : -- `next-auth.session-token` (et variantes) -- `KEYCLOAK_SESSION`, `KEYCLOAK_IDENTITY`, `AUTH_SESSION_ID` (si même domaine) -- `logout_in_progress` (expire après 60s) - ---- - -### 6. **`components/main-nav.tsx`** (lignes 364-446) -**Rôle** : Bouton de déconnexion dans la navigation -**Cookies** : Même logique que `signout-handler.tsx` -- Appelle `clearAuthCookies()` et `clearKeycloakCookies()` -- Crée cookie `logout_in_progress` -- Même flow que `SignOutHandler` - ---- - -### 7. **`components/layout/layout-wrapper.tsx`** ⭐ **FICHIER CRITIQUE** -**Rôle** : Écoute les messages de logout depuis les iframes -**Cookies manipulés** : -- Même pattern que `signout-handler.tsx` -- Gère les logout déclenchés par les iframes via `postMessage` - -**Fonction clé** (lignes 23-112) : -```typescript -const handleMessage = async (event: MessageEvent) => { - if (event.data.type === 'KEYCLOAK_LOGOUT' || event.data.type === 'LOGOUT') { - // Même flow que signout-handler.tsx - clearAuthCookies(); - clearKeycloakCookies(); - // ... logout complet - } -}; -``` - -**Cookies** : Identique à `signout-handler.tsx` - ---- - -### 8. **`components/auth/auth-check.tsx`** -**Rôle** : Guard d'authentification côté client -**Cookies** : Aucune manipulation directe -- Utilise `useSession()` qui lit `next-auth.session-token` -- Redirige vers `/signin` si `status === "unauthenticated"` - ---- - -### 9. **`components/providers.tsx`** -**Rôle** : Wrapper `SessionProvider` pour NextAuth -**Cookies** : Aucune manipulation, fournit le contexte de session - ---- - -## 🛠️ FICHIERS UTILITAIRES - Gestion Sessions/Cookies - -### 10. **`lib/session.ts`** ⭐ **FICHIER CRITIQUE** -**Rôle** : Utilitaires pour gérer les cookies et sessions - -#### `clearAuthCookies()` (lignes 93-108) -**Cookies supprimés** : -```typescript -// Supprime SEULEMENT les cookies de session, PAS les cookies OAuth -if (cookieName.startsWith('next-auth.session-token') || - cookieName.startsWith('__Secure-next-auth.session-token') || - cookieName.startsWith('__Host-next-auth.session-token')) { - document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; -} -``` - -**Important** : Ne supprime **PAS** : -- `next-auth.csrf-token` (nécessaire pour OAuth) -- `next-auth.state` (nécessaire pour OAuth flow) -- `next-auth.callback-url` - -#### `clearKeycloakCookies()` (lignes 115-154) -**Cookies Keycloak tentés de supprimer** : -```typescript -const keycloakCookieNames = [ - 'KEYCLOAK_SESSION', - 'KEYCLOAK_SESSION_LEGACY', - 'KEYCLOAK_IDENTITY', - 'KEYCLOAK_IDENTITY_LEGACY', - 'AUTH_SESSION_ID', - 'KC_RESTART', - 'KC_RESTART_LEGACY' -]; -``` - -**Limitation** : Ces cookies sont sur le domaine Keycloak, donc **ne peuvent pas être supprimés** depuis le domaine du dashboard (même origine). Cette fonction tente plusieurs combinaisons domain/path mais échouera si Keycloak est sur un domaine différent. - -#### `invalidateServiceTokens()` (lignes 53-91) -**Cookies** : Aucun, invalide les tokens de services externes (RocketChat, Leantime) - ---- - -### 11. **`lib/keycloak.ts`** -**Rôle** : Client Admin Keycloak pour gestion serveur -**Cookies** : Aucune manipulation directe -- Utilisé par `/api/auth/end-sso-session` pour terminer la session SSO - ---- - -## 🌐 FICHIERS API - Endpoints Serveur - -### 12. **`app/api/auth/end-sso-session/route.ts`** ⭐ **FICHIER CRITIQUE** -**Rôle** : Termine la session SSO Keycloak via Admin API -**Cookies** : Aucune manipulation directe -- Utilise Keycloak Admin API pour logout utilisateur -- **Important** : Termine la session **realm-wide**, pas seulement client - -**Flow** : -1. Lit `next-auth.session-token` via `getServerSession()` -2. Extrait `idToken` de la session -3. Décode `idToken` pour obtenir `userId` -4. Appelle `adminClient.users.logout({ id: userId })` -5. Keycloak supprime **toutes** les sessions de l'utilisateur - -**Impact cookies** : -- Keycloak supprime ses cookies côté serveur -- Les cookies Keycloak deviennent invalides (mais restent dans le navigateur jusqu'à expiration) - ---- - -### 13. **`app/api/auth/refresh-keycloak-session/route.ts`** -**Rôle** : Rafraîchit la session Keycloak (si existe) -**Cookies** : Lit `next-auth.session-token` via `getServerSession()` - ---- - -## 📄 FICHIERS LAYOUT - Structure Application - -### 14. **`app/layout.tsx`** -**Rôle** : Layout racine avec vérification de session serveur -**Cookies** : Lit `next-auth.session-token` via `getServerSession(authOptions)` -- Passe `isAuthenticated` à `LayoutWrapper` -- Détermine si c'est la page signin - ---- - -### 15. **`app/components/responsive-iframe.tsx`** (lignes 109-153) -**Rôle** : Composant iframe avec écoute de messages logout -**Cookies** : Aucune manipulation directe -- Écoute `postMessage` depuis iframes -- Déclenche logout si message `KEYCLOAK_LOGOUT` reçu -- **Note** : Logique similaire à `layout-wrapper.tsx` mais dans le composant iframe - ---- - -## 📝 FICHIERS TYPES - Définitions TypeScript - -### 16. **`types/next-auth.d.ts`** -**Rôle** : Extensions TypeScript pour NextAuth -**Cookies** : Aucune manipulation, définit les types de session/JWT - ---- - -## 🔍 ANALYSE DÉTAILLÉE DES COOKIES - -### Cookies NextAuth - -#### 1. **`next-auth.session-token`** (ou variantes sécurisées) -- **Domaine** : Domaine du dashboard -- **Path** : `/` -- **HttpOnly** : `true` (sécurité) -- **Secure** : `true` (si HTTPS) -- **SameSite** : `Lax` (par défaut) -- **Contenu** : JWT encrypté contenant : - - `accessToken` (Keycloak) - - `refreshToken` (Keycloak) - - `idToken` (Keycloak) - - Données utilisateur (id, email, roles, etc.) -- **Durée** : 4 heures (configuré dans `options.ts`) -- **Créé** : Lors de `signIn()` réussi -- **Supprimé** : Lors de `signOut()` ou expiration -- **Variantes** : - - `__Secure-next-auth.session-token` (si HTTPS) - - `__Host-next-auth.session-token` (si domaine racine) - -#### 2. **`next-auth.csrf-token`** -- **Domaine** : Domaine du dashboard -- **Path** : `/` -- **HttpOnly** : `true` -- **Secure** : `true` (si HTTPS) -- **SameSite** : `Lax` -- **Contenu** : Token CSRF pour protection OAuth -- **Durée** : Session (supprimé à la fermeture du navigateur) -- **Créé** : Lors de la première requête OAuth -- **Supprimé** : À la fermeture du navigateur -- **Important** : **N'EST PAS supprimé** par `clearAuthCookies()` (nécessaire pour OAuth) - -#### 3. **`next-auth.state`** -- **Domaine** : Domaine du dashboard -- **Path** : `/` -- **HttpOnly** : `true` -- **Secure** : `true` (si HTTPS) -- **SameSite** : `Lax` -- **Contenu** : État OAuth pour validation du callback -- **Durée** : Court (pendant le flow OAuth) -- **Créé** : Lors de `signIn()` (début flow OAuth) -- **Supprimé** : Après validation du callback OAuth -- **Important** : **N'EST PAS supprimé** par `clearAuthCookies()` (nécessaire pour OAuth) - -#### 4. **`next-auth.callback-url`** -- **Domaine** : Domaine du dashboard -- **Path** : `/` -- **HttpOnly** : `true` -- **Secure** : `true` (si HTTPS) -- **SameSite** : `Lax` -- **Contenu** : URL de redirection après authentification -- **Durée** : Court (pendant le flow OAuth) -- **Créé** : Lors de `signIn()` avec `callbackUrl` -- **Supprimé** : Après redirection - -### Cookies Keycloak - -#### 1. **`KEYCLOAK_SESSION`** -- **Domaine** : Domaine Keycloak (peut être différent du dashboard) -- **Path** : `/` ou `/realms/{realm}` -- **HttpOnly** : `true` -- **Secure** : `true` (si HTTPS) -- **SameSite** : `Lax` ou `None` (pour cross-site) -- **Contenu** : Identifiant de session SSO Keycloak -- **Durée** : Configuré dans Keycloak (typiquement 30 min - quelques heures) -- **Créé** : Lors de l'authentification Keycloak -- **Supprimé** : Lors de logout Keycloak ou expiration -- **Problème** : **Ne peut pas être supprimé** depuis le dashboard si domaine différent - -#### 2. **`KEYCLOAK_IDENTITY`** -- **Domaine** : Domaine Keycloak -- **Path** : `/` ou `/realms/{realm}` -- **HttpOnly** : `true` -- **Secure** : `true` -- **SameSite** : `Lax` ou `None` -- **Contenu** : Identité utilisateur Keycloak -- **Durée** : Même que `KEYCLOAK_SESSION` -- **Créé** : Lors de l'authentification Keycloak -- **Supprimé** : Lors de logout Keycloak ou expiration - -#### 3. **`AUTH_SESSION_ID`** -- **Domaine** : Domaine Keycloak -- **Path** : `/` ou `/realms/{realm}` -- **HttpOnly** : `true` -- **Secure** : `true` -- **SameSite** : `Lax` ou `None` -- **Contenu** : ID de session d'authentification -- **Durée** : Court (pendant le flow d'authentification) -- **Créé** : Lors du début du flow d'authentification -- **Supprimé** : Après authentification réussie ou échec - -### Cookies Custom - -#### 1. **`logout_in_progress`** -- **Domaine** : Domaine du dashboard -- **Path** : `/` -- **HttpOnly** : `false` (accessible via JavaScript) -- **Secure** : `false` -- **SameSite** : Non défini -- **Contenu** : `"true"` -- **Durée** : 60 secondes (`max-age=60`) -- **Créé** : Lors de `signOut()` (dans `signout-handler.tsx`, `main-nav.tsx`, `layout-wrapper.tsx`) -- **Supprimé** : Expire après 60s ou manuellement -- **Usage** : Empêche l'auto-login après logout - ---- - -## 🔄 FLOW COMPLET DE LOGIN - -### Étape 1 : Utilisateur accède à `/signin` -**Fichier** : `app/signin/page.tsx` -**Cookies** : -- Vérifie si `next-auth.session-token` existe -- Si existe mais `status === "unauthenticated"` → Session invalidée -- Si n'existe pas → Nouvel utilisateur, déclenche auto-login - -### Étape 2 : Auto-login déclenché -**Fichier** : `app/signin/page.tsx` (ligne 118) -**Action** : `signIn("keycloak", { callbackUrl: "/" })` -**Cookies créés** : -- `next-auth.csrf-token` (par NextAuth) -- `next-auth.state` (par NextAuth) -- `next-auth.callback-url` (par NextAuth) - -### Étape 3 : Redirection vers Keycloak -**Fichier** : `app/api/auth/[...nextauth]/route.ts` → NextAuth interne -**URL** : `${KEYCLOAK_ISSUER}/protocol/openid-connect/auth?...&prompt=login` -**Cookies Keycloak créés** : -- `AUTH_SESSION_ID` (par Keycloak) - -### Étape 4 : Authentification Keycloak -**Fichier** : Keycloak serveur -**Cookies Keycloak créés** : -- `KEYCLOAK_SESSION` (session SSO) -- `KEYCLOAK_IDENTITY` (identité utilisateur) - -### Étape 5 : Callback OAuth -**Fichier** : `app/api/auth/callback/keycloak` (géré par NextAuth) -**Cookies** : -- `next-auth.state` vérifié et supprimé -- `next-auth.callback-url` lu et utilisé - -### Étape 6 : JWT Callback -**Fichier** : `app/api/auth/options.ts` → `jwt` callback (ligne 196) -**Cookies** : -- Lit `next-auth.session-token` (décrypté) -- Stocke tokens Keycloak dans le JWT -- **Crée** `next-auth.session-token` (nouveau JWT avec tokens) - -### Étape 7 : Session Callback -**Fichier** : `app/api/auth/options.ts` → `session` callback (ligne 283) -**Cookies** : Lit `next-auth.session-token` pour construire la session - -### Étape 8 : Redirection vers `/` -**Fichier** : `app/signin/page.tsx` (ligne 72) -**Cookies** : `next-auth.session-token` maintenant présent - -### Étape 9 : Initialisation Storage -**Fichier** : `app/signin/page.tsx` (lignes 126-158) -**Action** : Appelle `/api/storage/init` -**Cookies** : Utilise `next-auth.session-token` (via `getServerSession()`) - ---- - -## 🔄 FLOW COMPLET DE LOGOUT - -### Étape 1 : Utilisateur clique "Déconnexion" -**Fichiers** : -- `components/main-nav.tsx` (ligne 364) -- OU `components/auth/signout-handler.tsx` (ligne 11) -- OU `components/layout/layout-wrapper.tsx` (ligne 32) si message iframe - -**Cookies créés** : -- `logout_in_progress=true; path=/; max-age=60` (ligne 16/369/38) -- `sessionStorage.setItem('just_logged_out', 'true')` (ligne 14/367/37) - -### Étape 2 : Suppression cookies NextAuth -**Fichier** : `lib/session.ts` → `clearAuthCookies()` (ligne 93) -**Cookies supprimés** : -- `next-auth.session-token` (et variantes) -- **PAS** `next-auth.csrf-token` (nécessaire pour OAuth) -- **PAS** `next-auth.state` (nécessaire pour OAuth) - -### Étape 3 : Tentative suppression cookies Keycloak -**Fichier** : `lib/session.ts` → `clearKeycloakCookies()` (ligne 115) -**Cookies tentés de supprimer** : -- `KEYCLOAK_SESSION`, `KEYCLOAK_IDENTITY`, etc. -- **Limitation** : Échoue si Keycloak sur domaine différent - -### Étape 4 : Fin de session SSO via Admin API -**Fichier** : `app/api/auth/end-sso-session/route.ts` (ligne 15) -**Action** : `adminClient.users.logout({ id: userId })` -**Cookies** : -- Keycloak supprime **toutes** les sessions côté serveur -- Les cookies Keycloak deviennent invalides (mais restent dans le navigateur) - -### Étape 5 : SignOut NextAuth -**Fichier** : `components/auth/signout-handler.tsx` (ligne 52) -**Action** : `signOut({ callbackUrl: "/signin?logout=true", redirect: false })` -**Cookies supprimés** : -- `next-auth.session-token` (supprimé côté serveur) - -### Étape 6 : Redirection vers Keycloak Logout -**Fichier** : `components/auth/signout-handler.tsx` (ligne 58) -**URL** : `${KEYCLOAK_ISSUER}/protocol/openid-connect/logout?...&id_token_hint=...&kc_action=LOGOUT` -**Cookies Keycloak** : -- Keycloak supprime ses cookies (si même domaine ou cross-domain configuré) - -### Étape 7 : Redirection vers `/signin?logout=true` -**Fichier** : Keycloak → `app/signin/page.tsx` -**Cookies** : -- `next-auth.session-token` : Supprimé -- `KEYCLOAK_SESSION` : Peut encore exister (si domaine différent) -- `logout_in_progress` : Existe encore (60s) - -### Étape 8 : Détection logout dans signin -**Fichier** : `app/signin/page.tsx` (lignes 17-67) -**Cookies vérifiés** : -- `logout_in_progress` (ligne 19) -- `next-auth.session-token` (ligne 25-29) -- `sessionStorage.getItem('just_logged_out')` (ligne 20) - -**Comportement** : -- Si `logout=true` dans URL → Affiche message "Vous avez été déconnecté" -- Si cookie session existe mais invalide → Empêche auto-login -- Si pas de cookie session → Auto-login après 1s (nouvel utilisateur) - ---- - -## ⚠️ PROBLÈMES IDENTIFIÉS - -### Problème 1 : Cookies Keycloak non supprimables -**Fichier** : `lib/session.ts` → `clearKeycloakCookies()` -**Cause** : Cookies Keycloak sur domaine différent -**Impact** : Les cookies Keycloak persistent après logout dashboard -**Solution actuelle** : Appel à Keycloak logout endpoint avec `id_token_hint` - -### Problème 2 : Session SSO Keycloak peut persister -**Fichier** : `app/api/auth/options.ts` (ligne 154) -**Cause** : `prompt=login` force le prompt, mais si SSO session existe, Keycloak peut auto-authentifier -**Impact** : Utilisateur peut être reconnecté automatiquement sans credentials -**Solution actuelle** : `prompt=login` + appel Admin API pour terminer session SSO - -### Problème 3 : Détection session invalide complexe -**Fichier** : `app/signin/page.tsx` (lignes 17-67) -**Cause** : Logique complexe pour détecter si session invalidée vs nouvel utilisateur -**Impact** : Auto-login peut se déclencher incorrectement -**Solution actuelle** : Vérification multiple (cookies, sessionStorage, URL params) - -### Problème 4 : Race condition logout/login -**Fichier** : `app/signin/page.tsx` (lignes 69-124) -**Cause** : Auto-login avec délai de 1s peut se déclencher pendant logout -**Impact** : Utilisateur peut être reconnecté immédiatement après logout -**Solution actuelle** : Flags `logout_in_progress` et `session_invalidated` - ---- - -## 📊 RÉSUMÉ DES FICHIERS PAR CATÉGORIE - -### Configuration Core (2 fichiers) -1. `app/api/auth/[...nextauth]/route.ts` -2. `app/api/auth/options.ts` ⭐ - -### Pages (2 fichiers) -3. `app/signin/page.tsx` ⭐ -4. `app/signout/page.tsx` - -### Composants Auth (4 fichiers) -5. `components/auth/signout-handler.tsx` ⭐ -6. `components/auth/auth-check.tsx` -7. `components/auth/signin-form.tsx` (si existe) -8. `components/auth/login-card.tsx` (si existe) - -### Composants Layout (2 fichiers) -9. `components/layout/layout-wrapper.tsx` ⭐ -10. `components/providers.tsx` - -### Navigation (1 fichier) -11. `components/main-nav.tsx` ⭐ - -### Utilitaires (2 fichiers) -12. `lib/session.ts` ⭐ -13. `lib/keycloak.ts` - -### API Routes (2 fichiers) -14. `app/api/auth/end-sso-session/route.ts` ⭐ -15. `app/api/auth/refresh-keycloak-session/route.ts` - -### Layout Root (1 fichier) -16. `app/layout.tsx` - -### Iframe (1 fichier) -17. `app/components/responsive-iframe.tsx` - -### Types (1 fichier) -18. `types/next-auth.d.ts` - -**Total : 18 fichiers principaux** - ---- - -## 🎯 FICHIERS CRITIQUES (⭐) - -Les fichiers marqués ⭐ sont **critiques** pour le flow login/logout : - -1. **`app/api/auth/options.ts`** - Configuration NextAuth, callbacks JWT/session -2. **`app/signin/page.tsx`** - Logique complexe de détection logout/auto-login -3. **`components/auth/signout-handler.tsx`** - Flow complet de logout -4. **`lib/session.ts`** - Gestion des cookies (suppression) -5. **`components/layout/layout-wrapper.tsx`** - Écoute logout depuis iframes -6. **`components/main-nav.tsx`** - Bouton logout -7. **`app/api/auth/end-sso-session/route.ts`** - Termine session SSO Keycloak - ---- - -## 📝 NOTES IMPORTANTES - -1. **Cookies NextAuth** : Gérés automatiquement par NextAuth, pas besoin de manipulation manuelle sauf pour suppression -2. **Cookies Keycloak** : Ne peuvent pas être supprimés depuis le dashboard si domaine différent (limitation navigateur) -3. **Session SSO** : Doit être terminée via Admin API Keycloak pour être complètement supprimée -4. **Auto-login** : Logique complexe pour distinguer nouvel utilisateur vs session invalidée -5. **Iframe logout** : Communication via `postMessage` pour synchroniser logout - ---- - -**Document créé le** : $(date) -**Dernière mise à jour** : Analyse complète du workflow login/logout avec focus sur les cookies - diff --git a/LOGOUT_LOGIN_FLOW_TRACE.md b/LOGOUT_LOGIN_FLOW_TRACE.md deleted file mode 100644 index 42a74033..00000000 --- a/LOGOUT_LOGIN_FLOW_TRACE.md +++ /dev/null @@ -1,305 +0,0 @@ -# Full Logout/Login Flow Trace - -## Issue 1: No Credentials Asked After Logout/Login - -### Current Flow Trace - -#### Step 1: User Clicks Logout -``` -Location: components/main-nav.tsx (line 364) -Action: onClick handler triggered - -1. sessionStorage.setItem('just_logged_out', 'true') -2. document.cookie = 'logout_in_progress=true; path=/; max-age=60' -3. clearAuthCookies() - Clears NextAuth cookies client-side -4. signOut({ callbackUrl: '/signin?logout=true', redirect: false }) - → Calls NextAuth /api/auth/signout endpoint - → Clears NextAuth session cookie server-side -5. window.location.replace(keycloakLogoutUrl) - → Redirects to: ${KEYCLOAK_ISSUER}/protocol/openid-connect/logout - → Parameters: - - post_logout_redirect_uri: /signin?logout=true - - id_token_hint: -``` - -#### Step 2: Keycloak Logout Endpoint -``` -Location: Keycloak Server -URL: ${KEYCLOAK_ISSUER}/protocol/openid-connect/logout - -Expected Behavior: -- Keycloak should invalidate the session -- Keycloak should clear session cookies: - - KEYCLOAK_SESSION (main session cookie) - - KEYCLOAK_SESSION_LEGACY - - KEYCLOAK_IDENTITY (identity cookie) - - KEYCLOAK_IDENTITY_LEGACY - - AUTH_SESSION_ID - - KC_RESTART (if exists) - -ACTUAL BEHAVIOR (PROBLEM): -- Keycloak logout endpoint with id_token_hint SHOULD clear cookies -- BUT: Keycloak might have SSO session that persists across clients -- OR: Cookies might not be cleared if domain/path mismatch -- OR: Keycloak might set new cookies during redirect -``` - -#### Step 3: Redirect Back to Signin -``` -Location: app/signin/page.tsx -URL: /signin?logout=true - -1. Component mounts -2. useEffect checks for logout flag (line 16-45) - - Sets isLogoutRedirect.current = true - - Removes 'just_logged_out' from sessionStorage - - Clears OAuth params from URL -3. Shows logout message with "Se connecter" button -4. User clicks "Se connecter" button (line 143-148) - - Calls: signIn("keycloak", { callbackUrl: "/" }) -``` - -#### Step 4: Keycloak Authorization Request -``` -Location: NextAuth → Keycloak -URL: ${KEYCLOAK_ISSUER}/protocol/openid-connect/auth - -Parameters sent: -- client_id: KEYCLOAK_CLIENT_ID -- redirect_uri: ${NEXTAUTH_URL}/api/auth/callback/keycloak -- response_type: code -- scope: openid profile email roles -- state: -- code_challenge: (if PKCE enabled) - -KEYCLOAK BEHAVIOR: -1. Keycloak receives authorization request -2. Keycloak checks for existing session cookies -3. IF Keycloak session cookies still exist: - → Keycloak finds valid SSO session - → Keycloak auto-authenticates user (no login prompt) - → Keycloak redirects back with authorization code -4. IF Keycloak session cookies are cleared: - → Keycloak shows login page - → User enters credentials - → Keycloak creates new session - → Keycloak redirects back with authorization code - -PROBLEM IDENTIFIED: -- Keycloak logout endpoint might not be clearing ALL session cookies -- OR: Keycloak has SSO session that persists (separate from client session) -- OR: Keycloak sets new cookies during the logout redirect process -- OR: Browser is preserving cookies due to SameSite/domain issues -``` - -### Root Cause Analysis - -**Problem**: Keycloak SSO Session Persistence - -Keycloak maintains two types of sessions: -1. **Client Session** (per OAuth client) - Cleared by logout endpoint -2. **SSO Session** (realm-wide) - May persist even after client logout - -When you call: -``` -GET /protocol/openid-connect/logout?id_token_hint=...&post_logout_redirect_uri=... -``` - -Keycloak behavior: -- ✅ Clears the **client session** for that specific OAuth client -- ✅ Invalidates tokens for that client -- ❌ **MIGHT NOT** clear the **SSO session** (realm-wide session) -- ❌ **MIGHT NOT** clear all session cookies if cookies are set with different domain/path - -**Why SSO Session Persists:** -- Keycloak SSO session is realm-wide, not client-specific -- Multiple clients can share the same SSO session -- Logging out from one client doesn't necessarily log out from the realm -- The SSO session cookie (KEYCLOAK_SESSION) might persist - -**When User Clicks "Se connecter":** -1. Redirects to Keycloak authorization endpoint -2. Keycloak checks for SSO session cookie -3. If SSO session cookie exists → Auto-authenticates (no credentials asked) -4. If SSO session cookie cleared → Shows login page - ---- - -## Issue 2: Cannot Logout from Iframe Application - -### Current Flow Trace - -#### Step 1: User in Iframe Application -``` -Location: Iframe application (e.g., /parole, /gite, etc.) -State: -- Dashboard has NextAuth session -- Keycloak session cookies exist -- Iframe app authenticated via Keycloak cookies -``` - -#### Step 2: User Clicks Logout in Iframe -``` -Location: Iframe application's logout button -Action: Iframe app's logout handler - -Possible Scenarios: - -Scenario A: Iframe calls its own logout endpoint -- Iframe app might call: POST /api/logout (iframe app's endpoint) -- This might clear iframe app's session -- BUT: Keycloak session cookies might still exist -- Result: Iframe app logs out, but Keycloak session persists - -Scenario B: Iframe calls Keycloak logout -- Iframe app might call: GET ${KEYCLOAK_ISSUER}/protocol/openid-connect/logout -- Keycloak clears session cookies -- BUT: NextAuth dashboard session still exists -- Result: Keycloak session cleared, but dashboard still logged in - -Scenario C: Iframe doesn't have logout -- Iframe app might not have logout functionality -- User stuck in iframe with no way to logout -``` - -#### Step 3: What Happens After Iframe Logout -``` -If iframe calls Keycloak logout: -1. Keycloak invalidates session -2. Keycloak clears session cookies -3. NextAuth dashboard still has valid JWT (30-day expiration) -4. NextAuth doesn't know Keycloak session was cleared -5. Dashboard widgets still show (using NextAuth session) -6. Iframe apps can't authenticate (Keycloak cookies cleared) -``` - -### Root Cause Analysis - -**Problem**: No Communication Between Iframe and Dashboard - -When iframe app logs out: -1. **Iframe app** calls Keycloak logout → Clears Keycloak cookies -2. **Dashboard** doesn't know about this → NextAuth session still valid -3. **Dashboard widgets** continue to work (using NextAuth session) -4. **Iframe apps** can't authenticate (Keycloak cookies gone) - -**Why Dashboard Doesn't Know:** -- NextAuth session is independent of Keycloak session cookies -- NextAuth JWT has 30-day expiration -- No mechanism to detect Keycloak session invalidation from iframe -- Dashboard only detects invalidation when trying to refresh tokens - -**Why Iframe Can't Logout Properly:** -- Iframe apps rely on Keycloak cookies for SSO -- If iframe calls Keycloak logout, it clears cookies -- But dashboard session persists -- If iframe doesn't call logout, user can't logout from iframe -- No way for iframe to trigger dashboard logout - ---- - -## Key Findings - -### Finding 1: Keycloak SSO Session Persistence -- **Issue**: Keycloak logout endpoint might not clear SSO session -- **Evidence**: User auto-authenticates without credentials after logout -- **Root Cause**: SSO session cookie persists after client logout -- **Impact**: Security issue - user should be asked for credentials - -### Finding 2: Missing `prompt=login` Parameter -- **Issue**: When calling `signIn("keycloak")`, no `prompt=login` parameter is sent -- **Evidence**: Keycloak auto-authenticates if SSO session exists -- **Root Cause**: NextAuth Keycloak provider doesn't force login prompt -- **Impact**: User bypasses credential check - -### Finding 3: Iframe Logout Isolation -- **Issue**: Iframe logout doesn't affect dashboard session -- **Evidence**: Dashboard widgets still show after iframe logout -- **Root Cause**: No communication mechanism between iframe and dashboard -- **Impact**: Inconsistent logout state - -### Finding 4: No Cross-Origin Logout Communication -- **Issue**: Iframe can't trigger dashboard logout -- **Evidence**: User stuck in iframe after logout -- **Root Cause**: No postMessage or other communication mechanism -- **Impact**: Poor user experience - ---- - -## Flow Diagram - -### Current Logout Flow (Dashboard) -``` -User clicks logout - ↓ -Clear NextAuth cookies - ↓ -Call NextAuth signOut() - ↓ -Redirect to Keycloak logout - ↓ -Keycloak clears client session - ↓ -Keycloak MAY clear SSO session (not guaranteed) - ↓ -Redirect to /signin?logout=true - ↓ -User clicks "Se connecter" - ↓ -Redirect to Keycloak auth - ↓ -Keycloak checks SSO session - ↓ -IF SSO session exists → Auto-authenticate (NO CREDENTIALS) -IF SSO session cleared → Show login page -``` - -### Current Iframe Logout Flow -``` -User in iframe clicks logout - ↓ -Iframe app calls logout (varies by app) - ↓ -IF calls Keycloak logout: - → Keycloak clears session cookies - → Dashboard session still valid - → Widgets still show - → Iframe can't authenticate -IF doesn't call logout: - → User stuck in iframe - → No way to logout -``` - ---- - -## Recommendations - -### For Issue 1 (No Credentials After Logout) -1. **Add `prompt=login` parameter** to Keycloak authorization request - - Forces Keycloak to show login page even if SSO session exists - - Location: `app/api/auth/options.ts` - KeycloakProvider authorization params - -2. **Clear Keycloak SSO session explicitly** - - Add `kc_action=LOGOUT` parameter to logout URL - - Or call Keycloak admin API to end SSO session - -3. **Clear Keycloak cookies client-side** - - After Keycloak logout redirect, clear any remaining Keycloak cookies - - Check for cookies with Keycloak domain - -### For Issue 2 (Iframe Logout) -1. **Implement postMessage communication** - - Iframe sends logout message to parent - - Dashboard listens for logout messages - - Dashboard triggers logout when iframe logs out - -2. **Detect Keycloak session invalidation** - - Poll Keycloak session status - - Detect when Keycloak cookies are cleared - - Automatically logout dashboard - -3. **Unified logout endpoint** - - Create API endpoint that logs out both dashboard and Keycloak - - Iframe apps call this endpoint - - Ensures synchronized logout - diff --git a/LOG_ANALYSIS_FEEDBACK.md b/LOG_ANALYSIS_FEEDBACK.md deleted file mode 100644 index cdf1348c..00000000 --- a/LOG_ANALYSIS_FEEDBACK.md +++ /dev/null @@ -1,239 +0,0 @@ -# Log Analysis & Feedback Report - -**Date**: 2026-01-01 -**Log File**: `log` -**Analysis Scope**: Application startup, notifications, session management, API calls - ---- - -## 🔴 Critical Issues - -### 1. Excessive Session Callback Logging (HIGH PRIORITY) - -**Problem**: -- **10+ session callbacks** triggered in a short period -- Each `getServerSession()` call triggers verbose logging -- Logs show `=== SESSION CALLBACK START ===` and `=== SESSION CALLBACK END ===` repeatedly - -**Root Cause**: -- Every API route calls `getServerSession(authOptions)` -- Root layout (`app/layout.tsx`) also calls it -- Session callback has extensive logging (lines 407-415 in `app/api/auth/options.ts`) - -**Impact**: -- ⚠️ **Performance**: Unnecessary logging overhead on every request -- ⚠️ **Log Noise**: Makes it hard to find actual issues -- ⚠️ **Debugging**: Difficult to identify real problems - -**Recommendation**: -```typescript -// In app/api/auth/options.ts, line 405-415 -async session({ session, token }) { - try { - // Only log in development or when there's an error - if (process.env.NODE_ENV === 'development' || token.error) { - console.log('=== SESSION CALLBACK START ==='); - console.log('Token error:', token.error); - // ... rest of logging - } - - // Or use a debug flag - const DEBUG_SESSION = process.env.DEBUG_SESSION === 'true'; - if (DEBUG_SESSION) { - console.log('=== SESSION CALLBACK START ==='); - // ... logging - } - - // ... rest of callback - } -} -``` - -**Priority**: 🔴 **HIGH** - Should be fixed immediately - ---- - -### 2. Missing markAsRead/markAllAsRead Logs - -**Problem**: -- No API calls to `/api/notifications/[id]/read` or `/api/notifications/read-all` in the log -- User reported notification count not updating after marking as read - -**Possible Causes**: -1. User didn't actually mark notifications as read during this log session -2. API calls are failing silently (network errors, CORS, etc.) -3. Client-side code isn't calling the API correctly -4. API routes aren't logging their calls - -**Investigation Steps**: -1. Add logging to mark-as-read API routes: - ```typescript - // In app/api/notifications/[id]/read/route.ts - export async function POST(request: Request, context: { params: Promise<{ id: string }> }) { - console.log('[NOTIFICATION_API] Mark as read called', { id: context.params?.id }); - // ... rest of code - } - ``` - -2. Check browser console for client-side errors -3. Verify network tab shows the API calls being made -4. Test the mark-as-read functionality while monitoring logs - -**Priority**: 🟡 **MEDIUM** - Needs investigation - ---- - -## ✅ Positive Observations - -### 1. Notification Service Working Correctly -- ✅ Service initialized properly -- ✅ Count fetched: **100 total, 66 unread** -- ✅ List fetched: **20 notifications** -- ✅ Caching working: `Cached notification counts for user` -- ✅ Leantime adapter functioning correctly - -### 2. Infrastructure Healthy -- ✅ Redis connection successful -- ✅ Database queries working (Prisma) -- ✅ IMAP connection successful (633ms) -- ✅ External APIs responding (News, Leantime, Rocket.Chat) - -### 3. Data Flow -- ✅ Session creation successful -- ✅ User authentication working -- ✅ Token refresh logic functioning -- ✅ OAuth token management working - ---- - -## 📊 Performance Metrics from Log - -| Metric | Value | Status | -|--------|-------|--------| -| Redis Connection | ✅ Success | Good | -| IMAP Connection Time | 633ms | Acceptable | -| Notification Count Fetch | ✅ Success | Good | -| Notification List Fetch | ✅ Success | Good | -| Session Callbacks | 10+ in short period | ⚠️ Too many | -| Database Queries | ✅ Working | Good | - ---- - -## 🔧 Recommended Actions - -### Immediate (This Week) - -1. **Reduce Session Callback Logging** - - Add environment-based conditional logging - - Only log errors or use `DEBUG_SESSION` flag - - **File**: `app/api/auth/options.ts` - -2. **Add Logging to Mark-as-Read Endpoints** - - Add console.log to track when mark-as-read is called - - Log success/failure - - **Files**: - - `app/api/notifications/[id]/read/route.ts` - - `app/api/notifications/read-all/route.ts` - -3. **Test Notification Mark-as-Read Flow** - - Monitor logs while marking notifications as read - - Verify API calls are being made - - Check if cache invalidation is working - -### Short Term (Next Sprint) - -4. **Implement Request Deduplication** - - Use the `request-deduplication` utility - - Prevent duplicate API calls - - **Already planned in unified refresh system** - -5. **Add Performance Monitoring** - - Track API call frequency - - Monitor session callback frequency - - Alert on excessive calls - -6. **Optimize Session Access** - - Consider caching session data - - Reduce redundant `getServerSession()` calls - - Use session context where possible - ---- - -## 🐛 Potential Issues Not Visible in Log - -### 1. Client-Side Errors -- Browser console errors not captured in server logs -- Network request failures -- React component errors - -### 2. Cache Invalidation -- No logs showing cache invalidation after mark-as-read -- May need to verify `invalidateCache()` is being called - -### 3. Race Conditions -- Multiple simultaneous API calls -- State update conflicts -- Not visible in single-threaded log - ---- - -## 📝 Log Patterns Analysis - -### Session Callback Pattern -``` -=== SESSION CALLBACK START === -Token error: undefined -Has accessToken: true -Has refreshToken: true -Token role: [...] -Token sub: 203cbc91-61ab-47a2-95d2-b5e1159327d7 -Token email: a.tmiri@clm.foundation -... -✅ Session created successfully -=== SESSION CALLBACK END === -``` - -**Frequency**: Every API call that uses `getServerSession()` - -**Recommendation**: Reduce to error-only logging - ---- - -### Notification Service Pattern -``` -[NOTIFICATION_SERVICE] getNotificationCount called for user ... -[LEANTIME_ADAPTER] getNotificationCount called for userId: ... -[LEANTIME_ADAPTER] Notification counts: { total: 100, unread: 66 } -[NOTIFICATION_SERVICE] Cached notification counts for user ... -``` - -**Status**: ✅ Working correctly - ---- - -## 🎯 Next Steps - -1. **Immediate**: Fix session callback logging (5 minutes) -2. **Today**: Add logging to mark-as-read endpoints (10 minutes) -3. **This Week**: Test notification mark-as-read flow end-to-end -4. **Next Sprint**: Implement unified refresh system (already planned) - ---- - -## 📌 Summary - -**Overall Assessment**: ✅ **System is functioning correctly** - -**Main Concerns**: -1. Excessive logging causing performance overhead -2. Missing visibility into mark-as-read operations -3. Need to verify notification count update flow - -**Confidence Level**: 🟢 **HIGH** - Core functionality working, minor optimizations needed - ---- - -**Generated**: 2026-01-01 -**Analyst**: AI Code Assistant -**Next Review**: After implementing fixes - diff --git a/LOG_FLOW_ANALYSIS.md b/LOG_FLOW_ANALYSIS.md deleted file mode 100644 index 18f93d46..00000000 --- a/LOG_FLOW_ANALYSIS.md +++ /dev/null @@ -1,342 +0,0 @@ -# Log Flow Analysis - Application Startup & Runtime - -**Date**: 2026-01-01 -**Log Source**: Application startup and initial page load -**Analysis Focus**: Flow patterns, errors, and system behavior - ---- - -## 🔍 Executive Summary - -**Overall Status**: 🟡 **MOSTLY HEALTHY** with one non-critical error - -**Key Findings**: -1. ⚠️ **Syntax Error**: Non-critical error during startup (doesn't block execution) -2. ✅ **Session Management**: Working correctly (5 session callbacks during startup) -3. ✅ **Notification Service**: Initialized and functioning (100 total, 66 unread) -4. ✅ **External Services**: All connecting successfully -5. ⚠️ **No Mark-as-Read Activity**: No API calls to mark notifications as read - ---- - -## 📊 Flow Breakdown - -### Phase 1: Application Startup (Lines 1-33) - -``` -1. Next.js starts (1313ms) -2. Redis connection established ✅ -3. Microsoft OAuth configuration loaded ✅ -4. ⚠️ SyntaxError: Unexpected identifier 'http' (line 29) -5. Redis connection warmed up ✅ -``` - -**Observations**: -- ✅ Startup is fast (1.3 seconds) -- ✅ Redis connection successful -- ⚠️ **Syntax Error** appears but doesn't block execution -- Error occurs between Redis warmup calls - -**Syntax Error Details**: -``` -⨯ SyntaxError: Unexpected identifier 'http' - at Object.Function [as get] () { - digest: '2421336728' - } -``` - -**Analysis**: -- Error is in a route handler (Function.get) -- Likely related to a route file with syntax issue -- Doesn't crash the application -- May be related to dynamic route generation - -**Recommendation**: Investigate route files for syntax errors, especially those using `http` in identifiers. - ---- - -### Phase 2: Initial Session Creation (Lines 34-71) - -``` -1. Session callback triggered -2. Token validation ✅ -3. User roles extracted ✅ -4. Session created successfully ✅ -``` - -**Session Details**: -- User ID: `203cbc91-61ab-47a2-95d2-b5e1159327d7` -- Email: `a.tmiri@clm.foundation` -- Roles: `['expression', 'entrepreneurship', 'admin', 'dataintelligence', 'mediation', 'mentors']` -- Tokens: Access token ✅, Refresh token ✅ - -**Status**: ✅ **HEALTHY** - ---- - -### Phase 3: Rocket.Chat Integration (Lines 72-91) - -``` -1. Rocket.Chat base URL: https://parole.slm-lab.net ✅ -2. Users list fetched (13 users) ✅ -3. User found: aminetmiri ✅ -4. Subscriptions filtered (1 room) ✅ -5. Messages fetched (5 messages) ✅ -6. Messages cached ✅ -7. ⚠️ "No valid session or email found" (line 92) -``` - -**Observations**: -- ✅ Rocket.Chat integration working -- ✅ User authentication successful -- ✅ Messages retrieved and cached -- ⚠️ Warning message at line 92 (may be from another service) - -**Status**: ✅ **HEALTHY** (warning is likely from a different service) - ---- - -### Phase 4: Additional Session Callbacks (Lines 93-169) - -``` -1. Session callback #2 (lines 93-130) -2. Session callback #3 (lines 132-169) -``` - -**Pattern**: Multiple session callbacks during initial page load - -**Frequency**: 3 session callbacks in ~40 lines of log - -**Analysis**: -- Normal behavior for Next.js with multiple API routes -- Each `getServerSession()` call triggers session callback -- All callbacks successful ✅ - -**Status**: ✅ **NORMAL** (but verbose logging as discussed) - ---- - -### Phase 5: Notification Service Initialization (Lines 170-246) - -``` -1. Notification service instance created ✅ -2. Leantime adapter initialized ✅ -3. Adapter registered ✅ -4. getNotificationCount called ✅ -5. Leantime API called ✅ -6. Response received (200) ✅ -7. Notifications parsed ✅ -8. Count calculated: 100 total, 66 unread ✅ -9. Counts cached ✅ -``` - -**Notification Details**: -- **Total**: 100 notifications -- **Unread**: 66 notifications -- **Source**: Leantime -- **Status**: ✅ **WORKING CORRECTLY** - -**Flow**: -``` -[NOTIFICATION_SERVICE] → [LEANTIME_ADAPTER] → Leantime API → Parse → Cache -``` - -**Status**: ✅ **HEALTHY** - ---- - -### Phase 6: Additional Operations (Lines 247-289) - -``` -1. IMAP pool status logged -2. Session callback #4 (lines 248-285) -3. Cached messages used -4. IMAP pool status logged again -``` - -**Observations**: -- ✅ IMAP connection pool healthy (0 active, max 20) -- ✅ Session callbacks continuing (normal) -- ✅ Caching working (messages from cache) - -**Status**: ✅ **HEALTHY** - ---- - -## 🔴 Issues Identified - -### 1. Syntax Error (Line 29) ⚠️ - -**Error**: -``` -⨯ SyntaxError: Unexpected identifier 'http' - at Object.Function [as get] () -``` - -**Impact**: -- ⚠️ **Low**: Doesn't crash application -- ⚠️ **Unknown**: May affect specific route -- ⚠️ **Non-blocking**: Application continues normally - -**Possible Causes**: -1. Route file with syntax error -2. Dynamic route generation issue -3. Template literal or string interpolation problem -4. Environment variable parsing issue - -**Investigation Steps**: -1. Search codebase for routes using `http` as identifier -2. Check dynamic route files -3. Review route handlers for syntax errors -4. Check Next.js route generation - -**Priority**: 🟡 **MEDIUM** - Should be fixed but not blocking - ---- - -### 2. "No valid session or email found" (Line 92) ⚠️ - -**Message**: `No valid session or email found` - -**Context**: Appears after Rocket.Chat operations - -**Analysis**: -- May be from a different service/route -- Doesn't affect Rocket.Chat functionality -- Could be from email service or another API route - -**Investigation**: Check which service logs this message - -**Priority**: 🟡 **LOW** - Appears to be a warning, not an error - ---- - -### 3. No Mark-as-Read Activity ⚠️ - -**Observation**: No `[NOTIFICATION_API]` log entries - -**Expected**: Should see logs when user marks notifications as read - -**Possible Reasons**: -1. User hasn't tested mark-as-read yet -2. API calls not reaching server -3. Client-side errors preventing API calls - -**Status**: ⏳ **PENDING TESTING** - -**Action**: Test mark-as-read functionality and check for new log entries - ---- - -## ✅ Positive Observations - -### 1. Fast Startup -- ✅ Application ready in 1.3 seconds -- ✅ All services initialized quickly - -### 2. Session Management -- ✅ All session callbacks successful -- ✅ Token validation working -- ✅ User roles extracted correctly - -### 3. Notification Service -- ✅ Service initialized correctly -- ✅ Leantime adapter working -- ✅ API calls successful -- ✅ Caching functioning - -### 4. External Services -- ✅ Redis connected -- ✅ Rocket.Chat connected -- ✅ Leantime API responding -- ✅ IMAP pool healthy - ---- - -## 📈 Performance Metrics - -| Metric | Value | Status | -|--------|-------|--------| -| Startup Time | 1313ms | ✅ Good | -| Redis Connection | ✅ Success | ✅ Good | -| Session Callbacks | 5 during startup | ✅ Normal | -| Notification Count | 100 total, 66 unread | ✅ Working | -| Rocket.Chat | ✅ Connected | ✅ Good | -| IMAP Pool | 0/20 active | ✅ Healthy | - ---- - -## 🔄 Flow Patterns - -### Session Callback Pattern -``` -Every getServerSession() call → Session callback → Token validation → Session created -``` - -**Frequency**: 5 times during startup (normal for multi-route page) - -**Recommendation**: Conditional logging (as discussed in impact analysis) - ---- - -### Notification Service Pattern -``` -Service init → Adapter registration → API call → Parse → Cache -``` - -**Status**: ✅ Working correctly - ---- - -## 🎯 Recommendations - -### Immediate Actions - -1. **Investigate Syntax Error** 🔴 - - Search for route files with `http` identifier - - Check dynamic routes - - Fix syntax error - -2. **Test Mark-as-Read** 🟡 - - Mark a notification as read - - Check logs for `[NOTIFICATION_API]` entries - - Verify notification count updates - -3. **Identify "No valid session" Source** 🟡 - - Find which service logs this message - - Determine if it's an error or warning - - Fix if necessary - -### Future Improvements - -4. **Implement Conditional Session Logging** (as planned) - - Add `DEBUG_SESSION` flag - - Reduce production logging - - Keep error logging - -5. **Add Error Monitoring** - - Track syntax errors - - Monitor route handler failures - - Alert on critical errors - ---- - -## 📝 Summary - -**Overall Assessment**: 🟢 **HEALTHY** with minor issues - -**Critical Issues**: 0 -**Warnings**: 2 (syntax error, "no valid session" message) -**Working Correctly**: ✅ All core functionality - -**Next Steps**: -1. Fix syntax error (investigate route files) -2. Test mark-as-read functionality -3. Identify source of "no valid session" message -4. Proceed with conditional session logging (when ready) - ---- - -**Generated**: 2026-01-01 -**Status**: Ready for action items - diff --git a/LOG_SEARCH_INSTRUCTIONS.md b/LOG_SEARCH_INSTRUCTIONS.md deleted file mode 100644 index 56d946d8..00000000 --- a/LOG_SEARCH_INSTRUCTIONS.md +++ /dev/null @@ -1,96 +0,0 @@ -# Log Search Instructions - Mark All As Read - -**Purpose**: Find the exact error causing mark-all-as-read to fail - ---- - -## 🔍 What to Do - -After you do `rm -rf .next && npm run build && npm start` and test "mark all as read", please: - -### Option 1: Search for Specific Markers - -In your log output, search for these exact strings: - -```bash -# Search for the adapter start marker -grep "===== markAllAsRead START =====" log - -# Search for all notification service logs -grep "NOTIFICATION_SERVICE.*markAllAsRead" log - -# Search for all leantime adapter logs -grep "LEANTIME_ADAPTER.*markAllAsRead" log - -# Search for API logs -grep "NOTIFICATION_API.*Mark all as read" log -``` - -### Option 2: Provide Complete Log Snippet - -When you test "mark all as read", copy the **COMPLETE** log output from: -- **Before**: 5-10 lines before `[NOTIFICATION_API] Mark all as read endpoint called` -- **After**: 50-100 lines after the failure - -This will show us the full flow. - ---- - -## 🎯 What We're Looking For - -### Expected Log Sequence - -``` -[NOTIFICATION_API] Mark all as read endpoint called -[NOTIFICATION_API] Mark all as read - Processing { userId: '...', timestamp: '...' } -[NOTIFICATION_SERVICE] markAllAsRead called for user ... -[NOTIFICATION_SERVICE] Available adapters: leantime -[NOTIFICATION_SERVICE] Processing adapter: leantime -[NOTIFICATION_SERVICE] Adapter leantime is configured: true -[NOTIFICATION_SERVICE] Calling markAllAsRead on adapter leantime -[LEANTIME_ADAPTER] ===== markAllAsRead START ===== ← MUST APPEAR -[LEANTIME_ADAPTER] markAllAsRead called for userId: ... -[LEANTIME_ADAPTER] API URL: ... -[LEANTIME_ADAPTER] Has API Token: true -[LEANTIME_ADAPTER] markAllAsRead - User email: ... -[LEANTIME_ADAPTER] markAllAsRead - Leantime user ID: ... -[LEANTIME_ADAPTER] markAllAsRead - Request body: {...} -[LEANTIME_ADAPTER] markAllAsRead - Response status: XXX -[LEANTIME_ADAPTER] markAllAsRead - Response body: {...} -[LEANTIME_ADAPTER] markAllAsRead - API Error: {...} ← This will show the actual error -[NOTIFICATION_SERVICE] Adapter leantime markAllAsRead result: false -``` - ---- - -## ❓ Questions - -1. **Do you see `[NOTIFICATION_SERVICE] markAllAsRead called for user`?** - - If NO → Service layer not being called - - If YES → Continue to next question - -2. **Do you see `[NOTIFICATION_SERVICE] Calling markAllAsRead on adapter leantime`?** - - If NO → Adapter not being called - - If YES → Continue to next question - -3. **Do you see `===== markAllAsRead START =====`?** - - If NO → Adapter method not executing (very strange!) - - If YES → We'll see the Leantime API error - ---- - -## 🔧 Quick Test - -After restart, run this command to see if the marker appears: - -```bash -# Test mark all as read, then immediately: -tail -n 200 log | grep -A 50 "Mark all as read" -``` - -This will show the last 200 lines of the log, filtered for mark-all-as-read operations, with 50 lines of context after each match. - ---- - -**Status**: Enhanced logging with multiple output methods. Awaiting complete log output to identify the exact failure point. - diff --git a/MARK_ALL_READ_CACHE_ISSUE.md b/MARK_ALL_READ_CACHE_ISSUE.md deleted file mode 100644 index 499d9061..00000000 --- a/MARK_ALL_READ_CACHE_ISSUE.md +++ /dev/null @@ -1,140 +0,0 @@ -# Mark All As Read - Cache Issue Analysis - -**Date**: 2026-01-01 -**Issue**: After marking all as read, list is empty but count still shows 66 - ---- - -## 🔍 Problem Analysis - -### Current Flow - -1. **User clicks "Mark all as read"** -2. **`markAllAsRead()` is called** -3. **Fetches notifications**: `this.getNotifications(userId, 1, 1000)` - - ⚠️ **PROBLEM**: This goes through `NotificationService.getNotifications()` - - ⚠️ **PROBLEM**: Which uses **CACHED** data if available - - ⚠️ **PROBLEM**: Cached notifications still have `isRead: false` -4. **Filters unread**: Gets 66 unread from cached data -5. **Marks each as read**: Calls Leantime API for each -6. **Invalidates cache**: After marking completes -7. **Count is fetched**: But might use stale cache or be fetched before invalidation - -### The Issue - -**Cache Race Condition**: -- `markAllAsRead` uses cached notifications (which are stale) -- Marks them as read in Leantime -- Invalidates cache -- But count might be fetched from cache **before** invalidation completes -- Or count cache might not be properly invalidated - -**Why List is Empty**: -- After marking, all notifications are read -- List might filter to show only unread -- So list is empty (correct behavior) -- But count still shows 66 (stale cache) - ---- - -## 🔧 Root Causes - -### 1. Using Cached Data in `markAllAsRead` - -**Current Code**: -```typescript -// In markAllAsRead -const allNotifications = await this.getNotifications(userId, 1, 1000); -``` - -**Problem**: `getNotifications()` uses cache, so we're working with stale data. - -**Solution**: Fetch directly from Leantime API, bypassing cache. - ---- - -### 2. Cache Invalidation Timing - -**Current Flow**: -1. Mark all as read (uses cached data) -2. Invalidate cache -3. Count is fetched (might use stale cache if fetched too soon) - -**Problem**: Race condition between invalidation and count fetch. - -**Solution**: -- Invalidate cache **before** marking (or fetch fresh data) -- Force immediate count refresh after marking -- Add delay before count fetch to ensure cache is cleared - ---- - -### 3. Count Cache Not Properly Invalidated - -**Current Code**: -```typescript -if (success) { - await this.invalidateCache(userId); -} -``` - -**Problem**: If `markAllAsRead` fails partially, cache might not be invalidated. - -**Solution**: Always invalidate cache, even on partial success. - ---- - -## ✅ Recommended Fixes - -### Fix 1: Bypass Cache in `markAllAsRead` - -**Change**: Fetch notifications directly from Leantime API, not through cached service. - -**Implementation**: -- Add a method to fetch notifications directly from adapter (bypassing cache) -- Or add a `forceRefresh` parameter to `getNotifications` -- Or fetch directly in `markAllAsRead` using Leantime API - -### Fix 2: Always Invalidate Cache - -**Change**: Invalidate cache even if some notifications fail to mark. - -**Implementation**: -- Invalidate cache if **any** notifications were successfully marked -- Not just if **all** succeeded - -### Fix 3: Force Fresh Count After Marking - -**Change**: After marking, force an immediate fresh count fetch. - -**Implementation**: -- After `markAllAsRead` completes, immediately call `getNotificationCount()` with cache bypass -- Or add a delay before count fetch to ensure cache is cleared - ---- - -## 📊 Expected Behavior After Fixes - -### After Mark All As Read - -**Before**: -- List: Empty (all read) ✅ -- Count: 66 (stale cache) ❌ - -**After**: -- List: Empty (all read) ✅ -- Count: 0 (fresh data) ✅ - ---- - -## 🎯 Next Steps - -1. **Fix cache usage in `markAllAsRead`**: Fetch fresh data, not cached -2. **Improve cache invalidation**: Always invalidate, even on partial success -3. **Force count refresh**: Immediately fetch fresh count after marking -4. **Test**: Verify count updates correctly after marking - ---- - -**Status**: Analysis complete. Ready to implement fixes. - diff --git a/MARK_ALL_READ_DIAGNOSTIC.md b/MARK_ALL_READ_DIAGNOSTIC.md deleted file mode 100644 index 4ea7e250..00000000 --- a/MARK_ALL_READ_DIAGNOSTIC.md +++ /dev/null @@ -1,132 +0,0 @@ -# Mark All As Read - Diagnostic Guide - -**Issue**: Adapter returns `false` but no detailed logs appear - ---- - -## 🔍 Current Situation - -**What We See**: -``` -[NOTIFICATION_SERVICE] Adapter leantime markAllAsRead result: false -[NOTIFICATION_SERVICE] markAllAsRead results: [ false ] -[NOTIFICATION_SERVICE] markAllAsRead overall success: false -``` - -**What's Missing**: -- `[NOTIFICATION_SERVICE] markAllAsRead called for user ...` -- `[NOTIFICATION_SERVICE] Processing adapter: leantime` -- `[NOTIFICATION_SERVICE] Calling markAllAsRead on adapter leantime` -- `[LEANTIME_ADAPTER] ===== markAllAsRead START =====` ← **NEW: Very prominent marker** - ---- - -## 🚨 Possible Causes - -### 1. Server Not Fully Restarted -**Solution**: Do a **hard restart**: -```bash -# Stop completely -sudo npm stop -# Or kill the process -sudo pkill -f "next start" - -# Wait a few seconds -sleep 3 - -# Start fresh -sudo npm start -``` - -### 2. Next.js Build Cache -**Solution**: Clear cache and rebuild: -```bash -rm -rf .next -sudo npm start -``` - -### 3. Log Buffering/Filtering -**Solution**: Check if logs are being filtered. Look for ALL logs around the mark-all-as-read operation. - -### 4. Code Not Deployed -**Solution**: Verify the file was saved and the server picked it up. - ---- - -## ✅ What to Look For After Restart - -### Expected Complete Log Flow - -When you click "Mark all as read", you should see **ALL** of these logs: - -``` -[NOTIFICATION_API] Mark all as read endpoint called -[NOTIFICATION_API] Mark all as read - Processing { userId: '...', timestamp: '...' } -[NOTIFICATION_SERVICE] markAllAsRead called for user ... -[NOTIFICATION_SERVICE] Available adapters: leantime -[NOTIFICATION_SERVICE] Processing adapter: leantime -[NOTIFICATION_SERVICE] Adapter leantime is configured: true -[NOTIFICATION_SERVICE] Calling markAllAsRead on adapter leantime -[LEANTIME_ADAPTER] ===== markAllAsRead START ===== ← VERY PROMINENT -[LEANTIME_ADAPTER] markAllAsRead called for userId: ... -[LEANTIME_ADAPTER] API URL: ... -[LEANTIME_ADAPTER] Has API Token: true -[LEANTIME_ADAPTER] markAllAsRead - User email: ... -[LEANTIME_ADAPTER] markAllAsRead - Leantime user ID: ... -[LEANTIME_ADAPTER] markAllAsRead - Request body: {...} -[LEANTIME_ADAPTER] markAllAsRead - API URL: ... -[LEANTIME_ADAPTER] markAllAsRead - Response status: XXX -[LEANTIME_ADAPTER] markAllAsRead - Response body: {...} -[LEANTIME_ADAPTER] markAllAsRead - Parsed response: {...} -[LEANTIME_ADAPTER] markAllAsRead - API Error: {...} ← This will show the actual error -[LEANTIME_ADAPTER] ===== markAllAsRead END (success: false) ===== -[NOTIFICATION_SERVICE] Adapter leantime markAllAsRead result: false -[NOTIFICATION_SERVICE] markAllAsRead results: [ false ] -[NOTIFICATION_SERVICE] markAllAsRead overall success: false -[NOTIFICATION_SERVICE] Not invalidating caches - operation failed -[NOTIFICATION_API] Mark all as read - Failed { userId: '...', duration: '...ms' } -``` - ---- - -## 🎯 Critical Check - -**After restarting**, search your logs for: -``` -===== markAllAsRead START ===== -``` - -If you **DON'T** see this line, the adapter method is **NOT** being called, which means: -- Server not restarted properly -- Code not deployed -- Different code path being used - -If you **DO** see this line, we'll have all the details we need to fix the Leantime API call. - ---- - -## 📋 Action Items - -1. ✅ **Hard Restart Server** (stop completely, wait, start) -2. ✅ **Test Mark All As Read** -3. ✅ **Search logs for `===== markAllAsRead START =====`** -4. ✅ **Share ALL logs** from the mark-all-as-read operation -5. ✅ **Look for `API Error:`** in the logs (this will show what Leantime is returning) - ---- - -## 🔧 If Logs Still Don't Appear - -If after restart you still don't see the `===== markAllAsRead START =====` log: - -1. **Verify file was saved**: Check `lib/services/notifications/leantime-adapter.ts` line 220-224 -2. **Check for syntax errors**: Run `npm run build` or check for TypeScript errors -3. **Verify server is using the file**: Check if there are multiple versions or build artifacts -4. **Check log output**: Make sure you're looking at the right log file/stream - ---- - -**Status**: Enhanced logging with prominent markers added. Awaiting server restart and test. - -**Next**: After restart, the `===== markAllAsRead START =====` marker will confirm the method is being called, and we'll see the exact Leantime API error. - diff --git a/MICROSOFT_OAUTH_ANALYSIS.md b/MICROSOFT_OAUTH_ANALYSIS.md deleted file mode 100644 index e7e9417b..00000000 --- a/MICROSOFT_OAUTH_ANALYSIS.md +++ /dev/null @@ -1,153 +0,0 @@ -# Microsoft OAuth Token Management Analysis - -## Current Implementation - -### Token Storage Locations - -1. **Redis Cache** (Primary for OAuth tokens) - - **Location**: `lib/redis.ts` → `cacheEmailCredentials()` - - **TTL**: 24 hours (`TTL.CREDENTIALS = 60 * 60 * 24`) - - **Stored**: `accessToken`, `refreshToken`, `tokenExpiry`, `useOAuth` - - **Key Format**: `email:credentials:${userId}:${accountId}` - -2. **Prisma Database** (Schema has fields but NOT used for OAuth tokens) - - **Location**: `prisma/schema.prisma` → `MailCredentials` model - - **Fields Available**: `refresh_token`, `access_token`, `token_expiry`, `use_oauth` - - **Current Status**: ❌ **Tokens are NOT saved to Prisma** (only Redis) - - **Code Comment**: "OAuth fields don't exist" (but they DO exist in schema!) - -### Token Refresh Flow - -**Location**: `lib/services/token-refresh.ts` → `ensureFreshToken()` - -1. Checks Redis for credentials -2. Validates token expiry (5-minute buffer) -3. Refreshes token if needed via Microsoft API -4. **Updates Redis only** (not Prisma) -5. Returns new access token - -### Issues Identified - -#### 🔴 Critical Issue #1: Refresh Tokens Not Persisted to Database - -**Problem**: -- Refresh tokens are only stored in Redis with 24-hour TTL -- If Redis is cleared, restarted, or TTL expires, refresh tokens are **permanently lost** -- Microsoft refresh tokens can last up to **90 days** (or indefinitely with `offline_access` scope) -- Users would need to re-authenticate if Redis data is lost - -**Impact**: -- ❌ Not viable for long-term production use -- ❌ Data loss risk on Redis restarts -- ❌ No backup/recovery mechanism - -#### 🟡 Issue #2: Token Refresh Doesn't Update Database - -**Problem**: -- When tokens are refreshed, only Redis is updated -- Prisma database still has old/expired tokens (if any) -- Schema has the fields but they're never populated - -**Impact**: -- ⚠️ Inconsistency between Redis and Database -- ⚠️ Can't recover from Redis cache loss - -#### 🟡 Issue #3: Missing Refresh Token in Logs - -From your logs: -``` -hasRefreshToken: false -``` - -This suggests the refresh token might not be properly saved or retrieved. - -### Microsoft OAuth Token Lifespan - -- **Access Token**: ~1 hour (3600 seconds) -- **Refresh Token**: Up to 90 days (with `offline_access` scope) -- **Token Refresh**: Returns new access token, may return new refresh token - -### Required Scopes - -Current implementation uses: -```typescript -const REQUIRED_SCOPES = [ - 'offline_access', // ✅ Required for long-lived refresh tokens - 'https://outlook.office.com/IMAP.AccessAsUser.All', - 'https://outlook.office.com/SMTP.Send' -].join(' '); -``` - -✅ `offline_access` is included - this is correct for long-term use. - -## Recommendations - -### ✅ Fix #1: Persist Refresh Tokens to Prisma - -**Why**: Refresh tokens are critical for long-term access and should be persisted to database. - -**Implementation**: -1. Save `refresh_token` to Prisma `MailCredentials.refresh_token` field -2. Update `token_expiry` when tokens are refreshed -3. Keep access tokens in Redis (short-lived, can be regenerated) -4. Use Prisma as source of truth for refresh tokens - -### ✅ Fix #2: Update Database on Token Refresh - -**Why**: Keep database in sync with refreshed tokens. - -**Implementation**: -1. After refreshing tokens, update Prisma `MailCredentials` record -2. Update `access_token` and `token_expiry` fields -3. Update `refresh_token` if Microsoft returns a new one - -### ✅ Fix #3: Fallback to Database if Redis Missing - -**Why**: Recover from Redis cache loss. - -**Implementation**: -1. If Redis cache is empty, check Prisma for refresh token -2. Use Prisma refresh token to get new access token -3. Re-populate Redis cache - -## Long-Term Viability Assessment - -### Current State: ⚠️ **NOT VIABLE** for long-term production - -**Reasons**: -1. ❌ Refresh tokens only in volatile Redis cache -2. ❌ No persistence mechanism -3. ❌ Risk of data loss on Redis restart -4. ❌ No recovery mechanism - -### After Fixes: ✅ **VIABLE** for long-term production - -**With recommended fixes**: -1. ✅ Refresh tokens persisted to database -2. ✅ Redis used for fast access token retrieval -3. ✅ Database as source of truth -4. ✅ Recovery mechanism in place - -## Token Storage Strategy (Recommended) - -### Access Tokens -- **Storage**: Redis (fast, short-lived) -- **TTL**: 1 hour (matches Microsoft token expiry) -- **Purpose**: Fast IMAP/SMTP authentication - -### Refresh Tokens -- **Storage**: Prisma Database (persistent, long-term) -- **TTL**: None (stored indefinitely until revoked) -- **Purpose**: Long-term access, token renewal - -### Token Expiry -- **Storage**: Both Redis and Prisma -- **Purpose**: Know when to refresh tokens - -## Implementation Priority - -1. **HIGH**: Persist refresh tokens to Prisma -2. **HIGH**: Update Prisma on token refresh -3. **MEDIUM**: Add fallback to database if Redis missing -4. **LOW**: Add token encryption at rest (if required by compliance) - diff --git a/MICROSOFT_OAUTH_FIXES.md b/MICROSOFT_OAUTH_FIXES.md deleted file mode 100644 index 6f670254..00000000 --- a/MICROSOFT_OAUTH_FIXES.md +++ /dev/null @@ -1,130 +0,0 @@ -# Microsoft OAuth Token Management - Fixes Applied - -## Issues Fixed - -### ✅ Fix #1: Refresh Tokens Now Persisted to Prisma Database - -**Problem**: Refresh tokens were only stored in Redis (24-hour TTL), risking permanent loss. - -**Solution**: -- Refresh tokens are now saved to `MailCredentials.refresh_token` in Prisma -- Access tokens and expiry also persisted to database -- Database acts as source of truth for long-term token storage - -**Files Modified**: -- `lib/services/email-service.ts` - `saveUserEmailCredentials()` now saves OAuth tokens to Prisma - -### ✅ Fix #2: Database Updated on Token Refresh - -**Problem**: When tokens were refreshed, only Redis was updated, leaving database stale. - -**Solution**: -- Token refresh now updates both Redis AND Prisma -- New refresh tokens (if provided by Microsoft) are persisted -- Token expiry timestamp updated in database - -**Files Modified**: -- `lib/services/token-refresh.ts` - `ensureFreshToken()` now updates Prisma after refresh - -### ✅ Fix #3: Fallback to Database if Redis Missing - -**Problem**: If Redis cache was empty, system couldn't recover refresh tokens. - -**Solution**: -- If Redis cache miss, system checks Prisma database -- Retrieves refresh token from database -- Re-populates Redis cache for future use - -**Files Modified**: -- `lib/services/token-refresh.ts` - Added database fallback logic - -### ✅ Fix #4: OAuth Fields Retrieved from Database - -**Problem**: When loading credentials from database, OAuth fields were ignored. - -**Solution**: -- Database queries now include OAuth fields (`access_token`, `refresh_token`, `token_expiry`, `use_oauth`) -- Credentials object properly populated with OAuth data from database - -**Files Modified**: -- `lib/services/email-service.ts` - `getImapConnection()` now includes OAuth fields from database - -## Token Storage Strategy (Current) - -### Access Tokens -- **Primary**: Redis (fast access, 24-hour TTL) -- **Backup**: Prisma Database (persisted) -- **Lifespan**: ~1 hour (Microsoft default) - -### Refresh Tokens -- **Primary**: Prisma Database (persistent, long-term) -- **Cache**: Redis (24-hour TTL, for fast access) -- **Lifespan**: Up to 90 days (with `offline_access` scope) - -### Token Expiry -- **Storage**: Both Redis and Prisma -- **Purpose**: Determine when to refresh tokens - -## Long-Term Viability - -### ✅ NOW VIABLE for Production - -**Improvements**: -1. ✅ Refresh tokens persisted to database -2. ✅ Database updated on token refresh -3. ✅ Fallback mechanism if Redis fails -4. ✅ No data loss on Redis restart -5. ✅ Recovery mechanism in place - -## What Happens Now - -### When Adding Microsoft Account: -1. OAuth tokens saved to **both** Redis and Prisma -2. Refresh token stored in database for long-term access -3. Access token cached in Redis for fast retrieval - -### When Token Expires: -1. System checks Redis first (fast path) -2. If Redis miss, checks Prisma database (fallback) -3. Uses refresh token to get new access token -4. Updates **both** Redis and Prisma with new tokens -5. Continues normal operation - -### If Redis is Cleared: -1. System detects Redis cache miss -2. Retrieves refresh token from Prisma database -3. Gets new access token using refresh token -4. Re-populates Redis cache -5. **No user action required** ✅ - -## Testing Recommendations - -1. **Test Token Refresh**: - - Wait for access token to expire (~1 hour) - - Verify system automatically refreshes - - Check both Redis and Prisma are updated - -2. **Test Redis Failure**: - - Clear Redis cache - - Try to access email - - Verify system recovers from database - -3. **Test Long-Term Access**: - - Wait several days - - Verify refresh token still works - - Check no re-authentication required - -## Monitoring - -Watch for these log messages: -- ✅ `Token for ${email} persisted to Prisma database` - Token saved successfully -- ✅ `Recovered credentials from Prisma and cached in Redis` - Fallback working -- ⚠️ `Error persisting tokens to database` - Database update failed (check logs) - -## Next Steps - -1. **Monitor**: Watch logs for token refresh operations -2. **Verify**: Check Prisma database has `refresh_token` values -3. **Test**: Verify email access works after Redis restart -4. **Optional**: Consider encrypting tokens at rest (if compliance requires) - diff --git a/MISSIONS_CENTRALE_WORKFLOW_ANALYSIS.md b/MISSIONS_CENTRALE_WORKFLOW_ANALYSIS.md deleted file mode 100644 index 1955b506..00000000 --- a/MISSIONS_CENTRALE_WORKFLOW_ANALYSIS.md +++ /dev/null @@ -1,930 +0,0 @@ -# Analyse Complète : Pages Missions et Centrale - Workflow Complet - -## 📋 Table des Matières -1. [Vue d'ensemble](#vue-densemble) -2. [Architecture des Pages](#architecture-des-pages) -3. [Workflow de Navigation](#workflow-de-navigation) -4. [Workflow de Création de Mission](#workflow-de-création-de-mission) -5. [Workflow de Consultation](#workflow-de-consultation) -6. [API Routes](#api-routes) -7. [Base de Données](#base-de-données) -8. [Intégrations Externes](#intégrations-externes) -9. [Stockage de Fichiers](#stockage-de-fichiers) -10. [Composants Réutilisables](#composants-réutilisables) - ---- - -## 🎯 Vue d'ensemble - -### Page "Centrale" -- **Route**: `/missions` -- **Nom dans le menu**: "Centrale" -- **Accès**: Rôles `entrepreneurship` ou `admin` (défini dans `components/main-nav.tsx`) -- **Description**: Centre d'Administration et de Pilotage (CAP) - Interface principale pour gérer les missions - -### Page "Missions" -- **Route principale**: `/missions` -- **Sous-routes**: - - `/missions` - Liste des missions de l'utilisateur - - `/missions/new` - Création d'une nouvelle mission - - `/missions/[missionId]` - Détails d'une mission - - `/missions/[missionId]/edit` - Édition d'une mission - -### Page "Mission Tab" (Tableau des Missions) -- **Route**: `/mission-tab` -- **Description**: Vue publique de toutes les missions disponibles -- **Sous-routes**: - - `/mission-tab` - Liste de toutes les missions - - `/mission-tab/[missionId]` - Détails d'une mission (vue publique) - ---- - -## 🏗️ Architecture des Pages - -### 1. Layout Principal - Missions (`app/missions/layout.tsx`) - -**Structure**: -``` -┌─────────────────────────────────────────┐ -│ Sidebar (CAP) - Fond rose clair │ -│ ┌───────────────────────────────────┐ │ -│ │ CAP │ │ -│ │ Centre d'Administration et de │ │ -│ │ Pilotage │ │ -│ └───────────────────────────────────┘ │ -│ • Mes Missions (/missions) │ -│ • Nouvelle Mission (/missions/new) │ -└─────────────────────────────────────────┘ -│ Contenu Principal (children) │ -└─────────────────────────────────────────┘ -``` - -**Fonctionnalités**: -- Sidebar fixe avec navigation -- Fond rose clair (`bg-pink-50`) pour la sidebar -- Fond blanc pour le contenu principal -- Navigation active highlightée - -### 2. Page Liste des Missions (`app/missions/page.tsx`) - -**Fonctionnalités**: -- Affichage en grille (responsive: 1/2/3 colonnes) -- Recherche par nom, niveau, type, ODD scope -- Filtrage en temps réel -- Cartes de mission avec: - - Logo (ou initiales si pas de logo) - - Nom de la mission - - Badge niveau (A/B/C/S) avec couleurs - - Icône ODD (Objectifs de Développement Durable) - - Services associés - - Description (intention) tronquée - - Date de création - - Bouton "Voir détails" - -**API utilisée**: `GET /api/missions` -- Retourne uniquement les missions où l'utilisateur est: - - Créateur (`creatorId`) - - Ou membre (`missionUsers`) - -### 3. Page Création de Mission (`app/missions/new/page.tsx`) - -**Composant principal**: `MissionsAdminPanel` -- Formulaire multi-onglets (5 onglets) -- Navigation séquentielle avec boutons Précédent/Suivant - -### 4. Page Détails Mission (`app/missions/[missionId]/page.tsx`) - -**Fonctionnalités**: -- Affichage complet des informations -- Logo de la mission -- Grille d'informations (Type, Donneur d'ordre, Durée, Niveau, Participation, ODD) -- Description complète -- Liste des documents/attachments -- Profils recherchés -- Services -- Bouton de suppression (si créateur ou admin) - -**API utilisée**: `GET /api/missions/[missionId]` - -### 5. Page Mission Tab (`app/mission-tab/page.tsx`) - -**Différences avec `/missions`**: -- Affiche **TOUTES** les missions (pas de filtre utilisateur) -- API utilisée: `GET /api/missions/all` -- Vue publique pour découvrir toutes les missions disponibles - ---- - -## 🔄 Workflow de Navigation - -### Accès à la Centrale - -``` -1. Utilisateur connecté avec rôle "entrepreneurship" ou "admin" - ↓ -2. Menu déroulant utilisateur (MainNav) - ↓ -3. Clic sur "Centrale" (href: '/missions') - ↓ -4. Redirection vers /missions - ↓ -5. Layout Missions s'affiche avec sidebar CAP - ↓ -6. Page Liste des Missions (/missions/page.tsx) -``` - -### Navigation dans la Centrale - -``` -┌─────────────────────────────────────────┐ -│ Sidebar CAP │ -│ ├─ Mes Missions (/missions) │ -│ └─ Nouvelle Mission (/missions/new) │ -└─────────────────────────────────────────┘ - │ │ - │ │ - ▼ ▼ -┌─────────────────┐ ┌──────────────────┐ -│ Liste Missions │ │ Création Mission │ -│ │ │ │ -│ [Carte Mission] │ │ [Formulaire] │ -│ └─► Détails │ │ │ -└─────────────────┘ └──────────────────┘ - │ - ▼ -┌─────────────────┐ -│ Détails Mission │ -│ │ -│ [Éditer] │ -│ [Supprimer] │ -└─────────────────┘ -``` - ---- - -## 🚀 Workflow de Création de Mission - -### Étape 1: Accès au Formulaire -``` -User → /missions/new → MissionsAdminPanel -``` - -### Étape 2: Formulaire Multi-Onglets - -**Onglet 1: General** -- Nom de la mission (requis) -- Logo (upload) -- ODD scope (requis) - Sélection parmi 17 ODD -- Niveau (requis) - A/B/C/S -- Intention (requis) - Description avec éditeur de texte - -**Onglet 2: Details** -- Type de mission (requis) - Remote/Onsite/Hybrid -- Donneur d'ordre (requis) - Individu/ONG/Start-ups -- Projection (requis) - Short/Medium/Long term -- Services - Checkboxes (Gite, ArtLab, Calcul) -- Participation (requis) - Volontaire/Cooptation -- Profils - Checkboxes (DataIntelligence, Expression, Mediation, Investigation, Coding, Lean) - -**Onglet 3: Attachments** -- Upload de fichiers (PDF, DOC, DOCX, XLS, XLSX, JPG, JPEG, PNG) -- Liste des fichiers sélectionnés -- Upload immédiat vers Minio (bucket 'missions') - -**Onglet 4: Skills** -- Liste de compétences (non fonctionnel actuellement - placeholders) - -**Onglet 5: Membres** -- **Les Gardiens de l'Intention** (3 gardiens requis): - - Gardien du Temps - - Gardien de la Parole - - Gardien de la Mémoire -- **Volontaires** (optionnel) -- Recherche d'utilisateurs ou groupes -- Assignation de rôles - -### Étape 3: Validation et Soumission - -**Validation**: -```typescript -const requiredFields = { - name: !!missionData.name, - oddScope: Array.isArray(missionData.oddScope) && missionData.oddScope.length > 0, - niveau: !!missionData.niveau, - intention: !!missionData.intention, - missionType: !!missionData.missionType, - donneurDOrdre: !!missionData.donneurDOrdre, - projection: !!missionData.projection, - participation: !!missionData.participation, - gardiens: gardienDuTemps !== null && - gardienDeLaParole !== null && - gardienDeLaMemoire !== null -} -``` - -**Soumission**: -```typescript -POST /api/missions -Body: { - name, oddScope, niveau, intention, missionType, - donneurDOrdre, projection, services, profils, - participation, guardians, volunteers, logo, attachments -} -``` - -### Étape 4: Traitement Backend - -**Séquence d'exécution**: - -1. **Création de la mission en base de données** - ```typescript - prisma.mission.create({ - data: { name, oddScope, niveau, intention, ... } - }) - ``` - -2. **Création des MissionUsers (gardiens + volontaires)** - ```typescript - prisma.missionUser.createMany({ - data: [ - { missionId, userId, role: 'gardien-temps' }, - { missionId, userId, role: 'gardien-parole' }, - { missionId, userId, role: 'gardien-memoire' }, - { missionId, userId, role: 'volontaire' }, // pour chaque volontaire - ] - }) - ``` - -3. **Upload du logo vers Minio** - - Path: `missions/{missionId}/logo{extension}` - - Bucket: `missions` - - Mise à jour du champ `logo` dans la mission - -4. **Upload des attachments vers Minio** - - Path: `missions/{missionId}/attachments/{filename}` - - Création des enregistrements `Attachment` en base - -5. **Vérification des fichiers dans Minio** - - Vérifie que tous les fichiers sont bien présents avant de continuer - -6. **Déclenchement du workflow N8N** - ```typescript - n8nService.triggerMissionCreation({ - ...missionData, - creatorId, - logoPath, - config: { N8N_API_KEY, MISSION_API_URL } - }) - ``` - -7. **Intégrations externes (via N8N)**: - - Création projet Leantime (si applicable) - - Création collection Outline (si applicable) - - Création canal RocketChat (si applicable) - - Création repository Gitea (si applicable) - - Création projet Penpot (si applicable) - -8. **Retour succès/erreur** - - Si succès: Redirection vers `/missions` - - Si erreur: Nettoyage des fichiers uploadés + message d'erreur - ---- - -## 👀 Workflow de Consultation - -### Consultation Liste des Missions - -**Route**: `/missions` ou `/mission-tab` - -**Flux**: -``` -1. Chargement de la page - ↓ -2. useEffect → fetch('/api/missions') ou fetch('/api/missions/all') - ↓ -3. Affichage du loader - ↓ -4. Réception des données - ↓ -5. Transformation des données: - - Ajout des logoUrl (si logo existe) - - Formatage des dates - - Calcul des couleurs de badges - - Extraction des infos ODD - ↓ -6. Filtrage par terme de recherche (si présent) - ↓ -7. Affichage en grille -``` - -**Recherche**: -- Filtre en temps réel sur: nom, niveau, type, ODD scope -- Pas de requête API supplémentaire (filtrage côté client) - -### Consultation Détails Mission - -**Route**: `/missions/[missionId]` ou `/mission-tab/[missionId]` - -**Flux**: -``` -1. Chargement de la page - ↓ -2. Récupération du missionId depuis les params - ↓ -3. useEffect → fetch(`/api/missions/${missionId}`) - ↓ -4. Affichage du loader - ↓ -5. Réception des données complètes: - - Mission avec tous les champs - - Creator (id, email) - - MissionUsers (avec user details) - - Attachments (avec publicUrl) - ↓ -6. Transformation: - - Ajout des logoUrl - - Formatage des dates - - Labels pour les types/niveaux - - URLs publiques pour les attachments - ↓ -7. Affichage des sections: - - Header avec nom et logo - - Grille d'informations - - Description - - Documents - - Profils recherchés - - Services - - Actions (Éditer/Supprimer) -``` - ---- - -## 🔌 API Routes - -### 1. `GET /api/missions` -**Fichier**: `app/api/missions/route.ts` - -**Fonctionnalité**: Liste les missions de l'utilisateur connecté - -**Filtres**: -- `limit` (default: 10) -- `offset` (default: 0) -- `search` (recherche dans name et intention) -- `name` (filtre exact) - -**Where Clause**: -```typescript -{ - OR: [ - { creatorId: userId }, - { missionUsers: { some: { userId } } } - ] -} -``` - -**Retour**: -```json -{ - "missions": [ - { - "id": "...", - "name": "...", - "logo": "missions/{id}/logo.png", - "logoUrl": "/api/missions/image/missions/{id}/logo.png", - "oddScope": ["odd-3"], - "niveau": "a", - "missionType": "remote", - "projection": "short", - "services": ["Gite"], - "intention": "...", - "createdAt": "...", - "creator": { "id": "...", "email": "..." }, - "missionUsers": [...], - "attachments": [...] - } - ], - "pagination": { - "total": 10, - "offset": 0, - "limit": 10 - } -} -``` - -### 2. `POST /api/missions` -**Fichier**: `app/api/missions/route.ts` - -**Fonctionnalité**: Crée une nouvelle mission - -**Body**: -```typescript -{ - name: string; - oddScope: string[]; - niveau?: string; - intention?: string; - missionType?: string; - donneurDOrdre?: string; - projection?: string; - services?: string[]; - profils?: string[]; - participation?: string; - guardians?: { - "gardien-temps": string; - "gardien-parole": string; - "gardien-memoire": string; - }; - volunteers?: string[]; - logo?: { - data: string; // base64 - name?: string; - type?: string; - }; - attachments?: Array<{ - data: string; // base64 - name?: string; - type?: string; - }>; -} -``` - -**Retour**: -```json -{ - "success": true, - "mission": { ... }, - "message": "Mission created successfully with all integrations" -} -``` - -### 3. `GET /api/missions/[missionId]` -**Fichier**: `app/api/missions/[missionId]/route.ts` - -**Fonctionnalité**: Récupère les détails d'une mission - -**Contrôle d'accès**: -- Utilisateur doit être créateur OU membre de la mission - -**Retour**: Mission complète avec relations - -### 4. `PUT /api/missions/[missionId]` -**Fichier**: `app/api/missions/[missionId]/route.ts` - -**Fonctionnalité**: Met à jour une mission - -**Contrôle d'accès**: -- Créateur OU gardien-temps/gardien-parole - -**Body**: Même structure que POST (tous les champs optionnels) - -### 5. `DELETE /api/missions/[missionId]` -**Fichier**: `app/api/missions/[missionId]/route.ts` - -**Fonctionnalité**: Supprime une mission - -**Contrôle d'accès**: -- Créateur OU admin uniquement - -**Actions**: -- Suppression du logo dans Minio -- Suppression de la mission en base (cascade sur MissionUsers et Attachments) -- TODO: Rollback N8N (non implémenté) - -### 6. `GET /api/missions/all` -**Fichier**: `app/api/missions/all/route.ts` - -**Fonctionnalité**: Liste TOUTES les missions (pas de filtre utilisateur) - -**Différences avec `/api/missions`**: -- Pas de filtre par utilisateur -- Retourne toutes les missions publiques -- Utilisé par `/mission-tab` - -### 7. `GET /api/missions/image/[...path]` -**Fichier**: `app/api/missions/image/[...path]/route.ts` - -**Fonctionnalité**: Sert les images (logos et attachments) depuis Minio - -**Path**: `missions/{missionId}/logo.png` ou `missions/{missionId}/attachments/{filename}` - -### 8. `POST /api/missions/upload` -**Fichier**: `app/api/missions/upload/route.ts` - -**Fonctionnalité**: Upload de fichiers (logo ou attachments) - -### 9. `GET /api/missions/[missionId]/attachments` -**Fichier**: `app/api/missions/[missionId]/attachments/route.ts` - -**Fonctionnalité**: Liste les attachments d'une mission - -### 10. `POST /api/missions/[missionId]/attachments` -**Fichier**: `app/api/missions/[missionId]/attachments/route.ts` - -**Fonctionnalité**: Ajoute un attachment à une mission existante - -### 11. `DELETE /api/missions/[missionId]/attachments/[attachmentId]` -**Fichier**: `app/api/missions/[missionId]/attachments/[attachmentId]/route.ts` - -**Fonctionnalité**: Supprime un attachment - ---- - -## 🗄️ Base de Données - -### Modèle Mission (`prisma/schema.prisma`) - -```prisma -model Mission { - id String @id @default(uuid()) - name String - logo String? // Path dans Minio - oddScope String[] // Catégories ODD - niveau String // A/B/C/S - intention String // Description - missionType String // remote/onsite/hybrid - donneurDOrdre String // individual/group/organization - projection String // short/medium/long - services String[] // ["Gite", "ArtLab", "Calcul"] - participation String? // volontaire/cooptation - profils String[] // ["DataIntelligence", ...] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - creator User @relation(fields: [creatorId], references: [id]) - creatorId String - attachments Attachment[] - missionUsers MissionUser[] - - // Intégrations externes - leantimeProjectId String? - outlineCollectionId String? - rocketChatChannelId String? - giteaRepositoryUrl String? - penpotProjectId String? - - @@index([creatorId]) -} -``` - -### Modèle MissionUser - -```prisma -model MissionUser { - id String @id @default(uuid()) - role String // 'gardien-temps', 'gardien-parole', 'gardien-memoire', 'volontaire' - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - mission Mission @relation(fields: [missionId], references: [id]) - missionId String - user User @relation(fields: [userId], references: [id]) - userId String - - @@unique([missionId, userId, role]) - @@index([missionId]) - @@index([userId]) -} -``` - -### Modèle Attachment - -```prisma -model Attachment { - id String @id @default(uuid()) - filename String - filePath String // Path dans Minio: missions/{missionId}/attachments/{filename} - fileType String // MIME type - fileSize Int - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - mission Mission @relation(fields: [missionId], references: [id]) - missionId String - uploader User @relation(fields: [uploaderId], references: [id]) - uploaderId String - - @@index([missionId]) - @@index([uploaderId]) -} -``` - ---- - -## 🔗 Intégrations Externes - -### Service N8N (`lib/services/n8n-service.ts`) - -**Webhook URL**: `https://brain.slm-lab.net/webhook/mission-created` - -**Données envoyées**: -```typescript -{ - name, oddScope, niveau, intention, missionType, - donneurDOrdre, projection, services, participation, - profils, guardians, volunteers, creatorId, - config: { - N8N_API_KEY, - MISSION_API_URL - } -} -``` - -**Workflow N8N déclenche**: -1. Création projet Leantime (si applicable) -2. Création collection Outline (si applicable) -3. Création canal RocketChat (si applicable) -4. Création repository Gitea (si applicable) -5. Création projet Penpot (si applicable) - -**Retour**: -```typescript -{ - success: boolean; - results?: { - leantimeProjectId?: string; - outlineCollectionId?: string; - rocketChatChannelId?: string; - giteaRepositoryUrl?: string; - penpotProjectId?: string; - failedServices?: { - gitRepo?: boolean; - leantimeProject?: boolean; - docCollection?: boolean; - rocketChatChannel?: boolean; - } - }; - error?: string; -} -``` - -**Rollback** (non implémenté): -- Webhook: `https://brain.slm-lab.net/webhook/mission-rollback` -- Appelé lors de la suppression d'une mission - ---- - -## 📦 Stockage de Fichiers - -### Minio Configuration - -**Endpoint**: `https://dome-api.slm-lab.net` -**Bucket**: `missions` -**Credentials**: Hardcodés dans `lib/mission-uploads.ts` (⚠️ à sécuriser) - -### Structure des Chemins - -**Logo**: -``` -missions/{missionId}/logo{extension} -Exemple: missions/abc-123/logo.png -``` - -**Attachments**: -``` -missions/{missionId}/attachments/{filename} -Exemple: missions/abc-123/attachments/document.pdf -``` - -### URLs Publiques - -**Format**: `/api/missions/image/{path}` - -**Exemples**: -- Logo: `/api/missions/image/missions/{missionId}/logo.png` -- Attachment: `/api/missions/image/missions/{missionId}/attachments/document.pdf` - -### Fonctions Utilitaires (`lib/mission-uploads.ts`) - -- `getMissionLogoPath()` - Génère le chemin du logo -- `getMissionAttachmentPath()` - Génère le chemin d'un attachment -- `uploadMissionLogo()` - Upload un logo vers Minio -- `uploadMissionAttachment()` - Upload un attachment vers Minio -- `deleteMissionLogo()` - Supprime un logo (TODO) -- `deleteMissionAttachment()` - Supprime un attachment -- `getMissionFileUrl()` - Construit l'URL publique -- `ensureMissionsPrefix()` - Normalise le chemin - ---- - -## 🧩 Composants Réutilisables - -### 1. `MissionsAdminPanel` (`components/missions/missions-admin-panel.tsx`) - -**Fonctionnalités**: -- Formulaire multi-onglets -- Gestion des gardiens et volontaires -- Upload de fichiers -- Validation complète -- Soumission vers API - -**Props**: Aucune (composant autonome) - -**State**: -- `missionData` - Données de la mission -- `selectedServices` - Services sélectionnés -- `selectedProfils` - Profils sélectionnés -- `gardienDuTemps`, `gardienDeLaParole`, `gardienDeLaMemoire` - IDs des gardiens -- `volontaires` - Array d'IDs de volontaires -- `activeTab` - Onglet actif -- `isSubmitting` - État de soumission - -### 2. `FileUpload` (`components/missions/file-upload.tsx`) - -**Fonctionnalités**: -- Upload de logo ou attachment -- Conversion en base64 -- Preview pour les images - -**Props**: -- `type`: 'logo' | 'attachment' -- `isNewMission`: boolean -- `onFileSelect`: (fileData) => void - -### 3. `AttachmentsList` (`components/missions/attachments-list.tsx`) - -**Fonctionnalités**: -- Liste des attachments d'une mission -- Upload de nouveaux attachments -- Suppression d'attachments - -**Props**: -- `missionId`: string -- `allowUpload`: boolean -- `allowDelete`: boolean - -### 4. `MissionsFrame` (`components/missions/missions-frame.tsx`) - -**Fonctionnalités**: Wrapper iframe (non utilisé actuellement) - ---- - -## 🔐 Contrôles d'Accès - -### Page Centrale (`/missions`) -- **Rôles requis**: `entrepreneurship` ou `admin` -- Vérifié dans `components/main-nav.tsx` via `hasRole()` - -### API Routes -- **Authentification**: Session NextAuth requise -- **GET /api/missions**: Missions où user est créateur ou membre -- **GET /api/missions/all**: Toutes les missions (authentifié) -- **GET /api/missions/[id]**: Créateur ou membre -- **PUT /api/missions/[id]**: Créateur ou gardien-temps/gardien-parole -- **DELETE /api/missions/[id]**: Créateur ou admin uniquement - ---- - -## 📊 Flux de Données Complet - -### Création de Mission - -``` -[Frontend] - MissionsAdminPanel - ↓ (soumission) - POST /api/missions - ↓ -[Backend] - 1. Validation - 2. prisma.mission.create() - 3. prisma.missionUser.createMany() - 4. uploadMissionLogo() → Minio - 5. uploadMissionAttachment() → Minio (pour chaque attachment) - 6. prisma.attachment.create() (pour chaque attachment) - 7. verifyFileExists() (vérification Minio) - 8. n8nService.triggerMissionCreation() - ↓ -[N8N Workflow] - - Création Leantime - - Création Outline - - Création RocketChat - - Création Gitea - - Création Penpot - ↓ -[Backend] - 9. Retour succès/erreur - ↓ -[Frontend] - Redirection → /missions -``` - -### Consultation de Mission - -``` -[Frontend] - MissionsPage ou MissionDetailPage - ↓ - fetch('/api/missions') ou fetch('/api/missions/[id]') - ↓ -[Backend] - 1. Vérification session - 2. Query Prisma avec relations - 3. Transformation des paths en URLs publiques - 4. Retour JSON - ↓ -[Frontend] - Affichage des données -``` - -### Affichage d'Image - -``` -[Frontend] - - ↓ -[Backend] - GET /api/missions/image/[...path] - ↓ - Lecture depuis Minio - ↓ - Stream vers client -``` - ---- - -## 🎨 Styles et UI - -### Couleurs des Badges Niveau -- **A (Apprentissage)**: `bg-green-100 text-green-800` -- **B (Basique)**: `bg-blue-100 text-blue-800` -- **C (Complexe)**: `bg-purple-100 text-purple-800` -- **S (Spécial)**: `bg-amber-100 text-amber-800` - -### Layout Sidebar CAP -- **Fond**: `bg-pink-50` -- **Bordure**: `border-pink-100` -- **Largeur**: `234px` fixe - -### Grille de Missions -- **Mobile**: 1 colonne -- **Tablet**: 2 colonnes (`md:grid-cols-2`) -- **Desktop**: 3 colonnes (`lg:grid-cols-3`) - ---- - -## 🐛 Points d'Attention - -1. **Credentials Minio hardcodés** dans `lib/mission-uploads.ts` - À déplacer vers variables d'environnement -2. **Rollback N8N non implémenté** lors de la suppression -3. **Skills tab non fonctionnel** - Placeholders uniquement -4. **Presigned URLs non implémentées** - Upload direct uniquement -5. **Gestion d'erreurs N8N** - Partielle (continue même si certaines intégrations échouent) - ---- - -## 📝 Notes Techniques - -### Types TypeScript - -**Mission Interface** (utilisée dans les pages): -```typescript -interface Mission { - id: string; - name: string; - logo?: string; - logoUrl?: string; - oddScope: string[]; - niveau: string; - missionType: string; - projection: string; - participation?: string; - services?: string[]; - profils?: string[]; - intention?: string; - donneurDOrdre?: string; - createdAt: string; - creator: User; - missionUsers: MissionUser[]; - attachments?: Attachment[]; -} -``` - -### Validation - -**Côté Frontend**: Validation dans `MissionsAdminPanel.validateMission()` -**Côté Backend**: Validation minimale (name et oddScope requis) - -### Gestion d'Erreurs - -- **Frontend**: Toast notifications via `useToast()` -- **Backend**: Retour JSON avec `error` et `details` -- **N8N**: Retour avec `success` et `failedServices` pour erreurs partielles - ---- - -## 🔄 Évolutions Possibles - -1. **Pagination** côté client pour les listes -2. **Filtres avancés** (par niveau, type, ODD, etc.) -3. **Recherche full-text** dans l'intention -4. **Export** des missions (PDF, CSV) -5. **Notifications** lors de l'assignation à une mission -6. **Statistiques** des missions -7. **Timeline** des activités d'une mission -8. **Commentaires** sur les missions -9. **États** des missions (brouillon, publiée, terminée, etc.) -10. **Permissions granulaires** par rôle de gardien - ---- - -**Document généré le**: $(date) -**Version**: 1.0 -**Auteur**: Analyse automatique du codebase - diff --git a/MISSION_CREATION_WORKFLOW_DETAILED.md b/MISSION_CREATION_WORKFLOW_DETAILED.md deleted file mode 100644 index c7a91d55..00000000 --- a/MISSION_CREATION_WORKFLOW_DETAILED.md +++ /dev/null @@ -1,982 +0,0 @@ -# Workflow Détaillé : Création de Mission - Prisma, Minio et N8N - -## 📋 Vue d'Ensemble - -Ce document trace **chaque étape** du workflow de création de mission, depuis le formulaire frontend jusqu'aux intégrations externes via N8N, en passant par Prisma (base de données) et Minio (stockage de fichiers). - ---- - -## 🔄 Flux Complet - Vue d'Ensemble - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. FRONTEND - MissionsAdminPanel │ -│ - Validation des champs │ -│ - Préparation des données (base64 pour fichiers) │ -│ - POST /api/missions │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 2. BACKEND - POST /api/missions │ -│ ├─ Authentification (NextAuth) │ -│ ├─ Validation des champs requis │ -│ ├─ STEP 1: Prisma.mission.create() │ -│ ├─ STEP 2: Prisma.missionUser.createMany() │ -│ ├─ STEP 3: Upload Logo → Minio │ -│ ├─ STEP 4: Upload Attachments → Minio │ -│ ├─ STEP 5: Vérification fichiers Minio │ -│ └─ STEP 6: N8N Workflow Trigger │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 3. N8N WORKFLOW │ -│ - Création Leantime Project │ -│ - Création Outline Collection │ -│ - Création RocketChat Channel │ -│ - Création Gitea Repository │ -│ - Création Penpot Project │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 📝 ÉTAPE 1 : Frontend - Préparation des Données - -### Fichier : `components/missions/missions-admin-panel.tsx` - -### 1.1 Validation (`validateMission()`) - -**Lignes 369-397** - -```typescript -const validateMission = () => { - const requiredFields = { - name: !!missionData.name, - oddScope: Array.isArray(missionData.oddScope) && missionData.oddScope.length > 0, - niveau: !!missionData.niveau, - intention: !!missionData.intention, - missionType: !!missionData.missionType, - donneurDOrdre: !!missionData.donneurDOrdre, - projection: !!missionData.projection, - participation: !!missionData.participation, - gardiens: gardienDuTemps !== null && - gardienDeLaParole !== null && - gardienDeLaMemoire !== null - }; - - // Vérifie que tous les champs requis sont remplis - // Retourne false si un champ manque -} -``` - -**Champs requis** : -- ✅ `name` : Nom de la mission -- ✅ `oddScope` : Array avec au moins 1 ODD -- ✅ `niveau` : A/B/C/S -- ✅ `intention` : Description -- ✅ `missionType` : remote/onsite/hybrid -- ✅ `donneurDOrdre` : individual/group/organization -- ✅ `projection` : short/medium/long -- ✅ `participation` : volontaire/cooptation -- ✅ `gardiens` : Les 3 gardiens doivent être assignés - -### 1.2 Préparation des Données (`handleSubmitMission()`) - -**Lignes 400-460** - -```typescript -const handleSubmitMission = async () => { - // 1. Validation - if (!validateMission()) return; - - // 2. Préparation des gardiens - const guardians = { - "gardien-temps": gardienDuTemps, - "gardien-parole": gardienDeLaParole, - "gardien-memoire": gardienDeLaMemoire - }; - - // 3. Construction de l'objet de soumission - const missionSubmitData = { - ...missionData, // name, oddScope, niveau, intention, etc. - services: selectedServices, // ["Gite", "ArtLab", "Calcul"] - profils: selectedProfils, // ["DataIntelligence", "Expression", ...] - guardians, // { "gardien-temps": userId, ... } - volunteers: volontaires, // [userId1, userId2, ...] - logo: missionData.logo // { data: "data:image/png;base64,...", name, type } - }; - - // 4. Envoi à l'API - const response = await fetch('/api/missions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(missionSubmitData) - }); -} -``` - -**Format du logo** (si présent) : -```typescript -logo: { - data: "data:image/png;base64,iVBORw0KGgoAAAANS...", // Base64 avec préfixe - name: "logo.png", - type: "image/png" -} -``` - -**Format des attachments** (si présents) : -```typescript -attachments: [ - { - data: "data:application/pdf;base64,JVBERi0xLjQKJe...", - name: "document.pdf", - type: "application/pdf" - } -] -``` - ---- - -## 🗄️ ÉTAPE 2 : Backend - POST /api/missions - -### Fichier : `app/api/missions/route.ts` - -### 2.1 Authentification et Validation - -**Lignes 205-224** - -```typescript -export async function POST(request: Request) { - // 1. Vérification de l'authentification - const { authorized, userId } = await checkAuth(request); - if (!authorized || !userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - // 2. Parsing du body - const body = await request.json(); - - // 3. Validation minimale (name et oddScope requis) - if (!body.name || !body.oddScope) { - return NextResponse.json({ - error: 'Missing required fields', - missingFields: ['name', 'oddScope'].filter(field => !body[field]) - }, { status: 400 }); - } -} -``` - -### 2.2 STEP 1 : Création de la Mission en Base de Données - -**Lignes 226-248** - -```typescript -// Préparation des données pour Prisma -const missionData = { - name: body.name, - oddScope: body.oddScope, // Array de strings - niveau: body.niveau, - intention: body.intention, - missionType: body.missionType, - donneurDOrdre: body.donneurDOrdre, - projection: body.projection, - services: body.services, // Array de strings - profils: body.profils, // Array de strings - participation: body.participation, - creatorId: userId, // ID de l'utilisateur connecté - logo: null, // Sera mis à jour après upload -}; - -// Création en base de données -const mission = await prisma.mission.create({ - data: missionData -}); - -// Résultat : mission.id est maintenant disponible -// Exemple : mission.id = "abc-123-def-456" -``` - -**Schéma Prisma** : -```prisma -model Mission { - id String @id @default(uuid()) - name String - logo String? // null pour l'instant - oddScope String[] - niveau String - intention String - missionType String - donneurDOrdre String - projection String - services String[] - participation String? - profils String[] - creatorId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - // Relations - creator User @relation(...) - missionUsers MissionUser[] - attachments Attachment[] -} -``` - -**Points importants** : -- ✅ La mission est créée **AVANT** l'upload des fichiers -- ✅ Le `mission.id` est généré automatiquement (UUID) -- ✅ Le champ `logo` est `null` pour l'instant -- ✅ Tous les champs sont sauvegardés sauf les fichiers - -### 2.3 STEP 2 : Création des MissionUsers (Gardiens + Volontaires) - -**Lignes 250-283** - -```typescript -// Préparation du tableau de MissionUsers -const missionUsers = []; - -// 2.1 Ajout des gardiens -if (body.guardians) { - for (const [role, guardianId] of Object.entries(body.guardians)) { - if (guardianId) { - missionUsers.push({ - missionId: mission.id, // ID de la mission créée - userId: guardianId, // ID de l'utilisateur gardien - role: role // "gardien-temps", "gardien-parole", "gardien-memoire" - }); - } - } -} - -// 2.2 Ajout des volontaires -if (body.volunteers && body.volunteers.length > 0) { - for (const volunteerId of body.volunteers) { - missionUsers.push({ - missionId: mission.id, - userId: volunteerId, - role: 'volontaire' - }); - } -} - -// 2.3 Création en batch dans Prisma -if (missionUsers.length > 0) { - await prisma.missionUser.createMany({ - data: missionUsers - }); -} -``` - -**Schéma Prisma MissionUser** : -```prisma -model MissionUser { - id String @id @default(uuid()) - role String // 'gardien-temps', 'gardien-parole', 'gardien-memoire', 'volontaire' - missionId String - userId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - mission Mission @relation(...) - user User @relation(...) - - @@unique([missionId, userId, role]) // Un utilisateur ne peut avoir qu'un rôle par mission -} -``` - -**Exemple de données créées** : -```typescript -missionUsers = [ - { missionId: "abc-123", userId: "user-1", role: "gardien-temps" }, - { missionId: "abc-123", userId: "user-2", role: "gardien-parole" }, - { missionId: "abc-123", userId: "user-3", role: "gardien-memoire" }, - { missionId: "abc-123", userId: "user-4", role: "volontaire" }, - { missionId: "abc-123", userId: "user-5", role: "volontaire" } -] -``` - -**Points importants** : -- ✅ Utilisation de `createMany()` pour performance (1 requête au lieu de N) -- ✅ Contrainte unique : un utilisateur ne peut avoir qu'un rôle par mission -- ✅ Les gardiens sont obligatoires, les volontaires optionnels - -### 2.4 STEP 3 : Upload du Logo vers Minio - -**Lignes 285-310** - -```typescript -let logoPath = null; -if (body.logo?.data) { - try { - // 3.1 Conversion base64 → Buffer → File - const base64Data = body.logo.data.split(',')[1]; // Retire "data:image/png;base64," - const buffer = Buffer.from(base64Data, 'base64'); - const file = new File( - [buffer], - body.logo.name || 'logo.png', - { type: body.logo.type || 'image/png' } - ); - - // 3.2 Upload vers Minio - const { filePath } = await uploadMissionLogo(userId, mission.id, file); - logoPath = filePath; // Ex: "missions/abc-123/logo.png" - uploadedFiles.push({ type: 'logo', path: filePath }); - - // 3.3 Mise à jour de la mission avec le chemin du logo - await prisma.mission.update({ - where: { id: mission.id }, - data: { logo: filePath } - }); - } catch (uploadError) { - throw new Error('Failed to upload logo'); - } -} -``` - -**Fonction `uploadMissionLogo()` - `lib/mission-uploads.ts`** : - -**Lignes 96-145** - -```typescript -export async function uploadMissionLogo( - userId: string, - missionId: string, - file: File -): Promise<{ filePath: string }> { - - // 1. Génération du chemin - const fileExtension = file.name.substring(file.name.lastIndexOf('.')); - const filePath = getMissionLogoPath(userId, missionId, fileExtension); - // Résultat : "missions/{missionId}/logo.png" - - // 2. Conversion pour Minio (retire le préfixe "missions/") - const minioPath = filePath.replace(/^missions\//, ''); - // Résultat : "{missionId}/logo.png" - - // 3. Conversion File → Buffer - const arrayBuffer = await file.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - // 4. Upload vers Minio via S3 SDK - await s3Client.send(new PutObjectCommand({ - Bucket: 'missions', // Bucket Minio - Key: minioPath, // "{missionId}/logo.png" - Body: buffer, // Contenu du fichier - ContentType: file.type, // "image/png" - ACL: 'public-read' // Accès public en lecture - })); - - return { filePath }; // Retourne le chemin complet avec préfixe -} -``` - -**Configuration Minio** : -```typescript -// lib/mission-uploads.ts -const s3Client = new S3Client({ - region: 'us-east-1', - endpoint: 'https://dome-api.slm-lab.net', // Endpoint Minio - credentials: { - accessKeyId: '4aBT4CMb7JIMMyUtp4Pl', - secretAccessKey: 'HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg' - }, - forcePathStyle: true // Requis pour MinIO -}); -``` - -**Structure dans Minio** : -``` -Bucket: missions - └── {missionId}/ - └── logo.png -``` - -**Points importants** : -- ✅ Le logo est uploadé **APRÈS** la création de la mission (pour avoir le missionId) -- ✅ Le chemin est mis à jour dans Prisma après l'upload -- ✅ Le fichier est stocké avec ACL `public-read` pour accès public -- ✅ Le chemin stocké inclut le préfixe `missions/` pour cohérence - -### 2.5 STEP 4 : Upload des Attachments vers Minio - -**Lignes 312-343** - -```typescript -if (body.attachments && body.attachments.length > 0) { - try { - // 4.1 Traitement parallèle de tous les attachments - const attachmentPromises = body.attachments.map(async (attachment: any) => { - // Conversion base64 → Buffer → File - const base64Data = attachment.data.split(',')[1]; - const buffer = Buffer.from(base64Data, 'base64'); - const file = new File( - [buffer], - attachment.name || 'attachment', - { type: attachment.type || 'application/octet-stream' } - ); - - // 4.2 Upload vers Minio - const { filePath, filename, fileType, fileSize } = - await uploadMissionAttachment(userId, mission.id, file); - uploadedFiles.push({ type: 'attachment', path: filePath }); - - // 4.3 Création de l'enregistrement Attachment en base - return prisma.attachment.create({ - data: { - missionId: mission.id, - filename, // "document.pdf" - filePath, // "missions/abc-123/attachments/document.pdf" - fileType, // "application/pdf" - fileSize, // 123456 (bytes) - uploaderId: userId - } - }); - }); - - // 4.4 Attente de tous les uploads (parallèle) - await Promise.all(attachmentPromises); - } catch (attachmentError) { - throw new Error('Failed to upload attachments'); - } -} -``` - -**Fonction `uploadMissionAttachment()` - `lib/mission-uploads.ts`** : - -**Lignes 148-210** - -```typescript -export async function uploadMissionAttachment( - userId: string, - missionId: string, - file: File -): Promise<{ - filename: string; - filePath: string; - fileType: string; - fileSize: number; -}> { - - // 1. Génération du chemin - const filePath = getMissionAttachmentPath(userId, missionId, file.name); - // Résultat : "missions/{missionId}/attachments/{filename}" - - // 2. Conversion pour Minio - const minioPath = filePath.replace(/^missions\//, ''); - // Résultat : "{missionId}/attachments/{filename}" - - // 3. Conversion File → Buffer - const arrayBuffer = await file.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - // 4. Upload vers Minio - await s3Client.send(new PutObjectCommand({ - Bucket: 'missions', - Key: minioPath, - Body: buffer, - ContentType: file.type, - ACL: 'public-read' - })); - - return { - filename: file.name, - filePath, // Chemin complet avec préfixe - fileType: file.type, - fileSize: file.size - }; -} -``` - -**Schéma Prisma Attachment** : -```prisma -model Attachment { - id String @id @default(uuid()) - filename String // "document.pdf" - filePath String // "missions/{missionId}/attachments/{filename}" - fileType String // "application/pdf" - fileSize Int // 123456 - missionId String - uploaderId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - mission Mission @relation(...) - uploader User @relation(...) -} -``` - -**Structure dans Minio** : -``` -Bucket: missions - └── {missionId}/ - ├── logo.png - └── attachments/ - ├── document1.pdf - ├── document2.docx - └── image.jpg -``` - -**Points importants** : -- ✅ Uploads en **parallèle** avec `Promise.all()` pour performance -- ✅ Chaque attachment crée un enregistrement Prisma séparé -- ✅ Le `uploaderId` est l'utilisateur qui a créé la mission -- ✅ Les fichiers sont stockés avec ACL `public-read` - -### 2.6 STEP 5 : Vérification des Fichiers dans Minio - -**Lignes 345-365** - -```typescript -// 5.1 Vérification du logo -if (logoPath) { - const logoExists = await verifyFileExists(logoPath); - if (!logoExists) { - throw new Error('Logo file not found in Minio'); - } -} - -// 5.2 Vérification des attachments -if (body.attachments?.length > 0) { - const attachmentVerifications = uploadedFiles - .filter(f => f.type === 'attachment') - .map(f => verifyFileExists(f.path)); - - const attachmentResults = await Promise.all(attachmentVerifications); - if (attachmentResults.some(exists => !exists)) { - throw new Error('One or more attachment files not found in Minio'); - } -} -``` - -**Fonction `verifyFileExists()` - Lignes 191-202** - -```typescript -async function verifyFileExists(filePath: string): Promise { - try { - await s3Client.send(new HeadObjectCommand({ - Bucket: 'missions', - Key: filePath.replace('missions/', '') // Retire le préfixe - })); - return true; // Fichier existe - } catch (error) { - return false; // Fichier n'existe pas - } -} -``` - -**Points importants** : -- ✅ Vérification **AVANT** de déclencher N8N -- ✅ Utilise `HeadObjectCommand` (légère, ne télécharge pas le fichier) -- ✅ Si un fichier manque, le workflow s'arrête avec erreur - -### 2.7 STEP 6 : Déclenchement du Workflow N8N - -**Lignes 367-393** - -```typescript -// 6.1 Préparation des données pour N8N -const n8nData = { - ...body, // Toutes les données de la mission - creatorId: userId, // ID du créateur - logoPath: logoPath, // Chemin du logo (ou null) - config: { - N8N_API_KEY: process.env.N8N_API_KEY, - MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL - } -}; - -// 6.2 Déclenchement du workflow -const n8nService = new N8nService(); -const workflowResult = await n8nService.triggerMissionCreation(n8nData); - -// 6.3 Vérification du résultat -if (!workflowResult.success) { - throw new Error(workflowResult.error || 'N8N workflow failed'); -} - -// 6.4 Retour succès -return NextResponse.json({ - success: true, - mission, - message: 'Mission created successfully with all integrations' -}); -``` - ---- - -## 🔗 ÉTAPE 3 : Service N8N - -### Fichier : `lib/services/n8n-service.ts` - -### 3.1 Configuration - -**Lignes 3-17** - -```typescript -export class N8nService { - private webhookUrl: string; - private rollbackWebhookUrl: string; - private apiKey: string; - - constructor() { - this.webhookUrl = process.env.N8N_WEBHOOK_URL || - 'https://brain.slm-lab.net/webhook/mission-created'; - this.rollbackWebhookUrl = process.env.N8N_ROLLBACK_WEBHOOK_URL || - 'https://brain.slm-lab.net/webhook/mission-rollback'; - this.apiKey = process.env.N8N_API_KEY || ''; - } -} -``` - -### 3.2 Nettoyage et Validation des Données - -**Lignes 19-49** - -```typescript -async triggerMissionCreation(data: any): Promise { - // Nettoyage des données - const cleanData = { - name: data.name, - oddScope: Array.isArray(data.oddScope) ? data.oddScope : [data.oddScope], - niveau: data.niveau || 'default', - intention: data.intention?.trim() || '', - missionType: data.missionType || 'default', - donneurDOrdre: data.donneurDOrdre || 'default', - projection: data.projection || 'default', - services: Array.isArray(data.services) ? data.services : [], - participation: data.participation || 'default', - profils: Array.isArray(data.profils) ? data.profils : [], - guardians: data.guardians || {}, - volunteers: Array.isArray(data.volunteers) ? data.volunteers : [], - creatorId: data.creatorId, - config: { - ...data.config, - N8N_API_KEY: this.apiKey, - MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL || 'https://api.slm-lab.net/api' - } - }; -} -``` - -**Points importants** : -- ✅ Normalisation des arrays (assure qu'ils sont bien des arrays) -- ✅ Valeurs par défaut pour éviter les undefined -- ✅ Trim de l'intention pour retirer les espaces -- ✅ Conservation de la config avec les clés API - -### 3.3 Envoi au Webhook N8N - -**Lignes 73-96** - -```typescript -const response = await fetch(this.webhookUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': this.apiKey // Authentification - }, - body: JSON.stringify(cleanData) -}); - -if (!response.ok) { - const errorText = await response.text(); - throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`); -} -``` - -### 3.4 Traitement de la Réponse - -**Lignes 98-143** - -```typescript -const responseText = await response.text(); - -try { - const result = JSON.parse(responseText); - - // Détection d'erreurs partielles - if (result.error || result.message?.includes('failed')) { - const errorMessage = result.message || result.error; - const failedServices = { - gitRepo: errorMessage.includes('Git repository creation failed'), - leantimeProject: errorMessage.includes('Leantime project creation failed'), - docCollection: errorMessage.includes('Documentation collection creation failed'), - rocketChatChannel: errorMessage.includes('RocketChat channel creation failed') - }; - - // Retourne succès avec erreurs partielles - return { - success: true, - results: { ...result, failedServices } - }; - } - - return { success: true, results: result }; - -} catch (parseError) { - // Si la réponse n'est pas JSON, considère comme succès - return { - success: true, - results: { - logoUrl: null, - leantimeProjectId: null, - outlineCollectionId: null, - rocketChatChannelId: null, - giteaRepositoryUrl: null - } - }; -} -``` - -**Format de réponse attendu de N8N** : -```json -{ - "leantimeProjectId": "project-123", - "outlineCollectionId": "collection-456", - "rocketChatChannelId": "channel-789", - "giteaRepositoryUrl": "https://git.slm-lab.net/mission-abc", - "penpotProjectId": "penpot-xyz" -} -``` - -**Intégrations créées par N8N** : -1. **Leantime** : Projet de gestion de projet -2. **Outline** : Collection de documentation -3. **RocketChat** : Canal de communication -4. **Gitea** : Repository Git -5. **Penpot** : Projet de design - -**Points importants** : -- ✅ Gestion des erreurs partielles (certains services peuvent échouer) -- ✅ Si la réponse n'est pas JSON, considère comme succès (workflow déclenché) -- ✅ Les IDs retournés ne sont **PAS** sauvegardés en base (TODO) - ---- - -## 🧹 Gestion des Erreurs et Cleanup - -### Fichier : `app/api/missions/route.ts` - Lignes 398-418 - -```typescript -catch (error) { - console.error('Error in mission creation:', error); - - // Cleanup: Suppression de tous les fichiers uploadés - for (const file of uploadedFiles) { - try { - await s3Client.send(new DeleteObjectCommand({ - Bucket: 'missions', - Key: file.path.replace('missions/', '') - })); - console.log('Cleaned up file:', file.path); - } catch (cleanupError) { - console.error('Error cleaning up file:', file.path, cleanupError); - } - } - - return NextResponse.json({ - error: 'Failed to create mission', - details: error instanceof Error ? error.message : String(error) - }, { status: 500 }); -} -``` - -**Scénarios de cleanup** : -1. ✅ Erreur lors de l'upload du logo → Suppression du logo -2. ✅ Erreur lors de l'upload d'un attachment → Suppression de tous les fichiers -3. ✅ Erreur lors de la vérification Minio → Suppression de tous les fichiers -4. ✅ Erreur N8N → Suppression de tous les fichiers - -**Points importants** : -- ✅ La mission reste en base même en cas d'erreur (orphan) -- ✅ Les MissionUsers restent en base même en cas d'erreur -- ✅ Seuls les fichiers Minio sont nettoyés -- ⚠️ **TODO** : Rollback complet (suppression mission + users si erreur) - ---- - -## 📊 Résumé des Opérations Prisma - -### Requêtes Prisma dans l'ordre d'exécution : - -1. **`prisma.mission.create()`** - - Crée la mission avec tous les champs - - Génère un UUID pour `id` - - `logo` = `null` initialement - -2. **`prisma.missionUser.createMany()`** - - Crée tous les gardiens et volontaires en une requête - - Utilise `createMany()` pour performance - -3. **`prisma.mission.update()`** (si logo) - - Met à jour le champ `logo` avec le chemin Minio - -4. **`prisma.attachment.create()`** (pour chaque attachment) - - Créé en parallèle avec `Promise.all()` - - Un enregistrement par fichier - -**Total** : 1 + 1 + (0 ou 1) + N requêtes Prisma -- Minimum : 2 requêtes (mission + users) -- Avec logo : 3 requêtes -- Avec N attachments : 3 + N requêtes - ---- - -## 📦 Résumé des Opérations Minio - -### Uploads Minio dans l'ordre d'exécution : - -1. **Logo** (si présent) - - Bucket : `missions` - - Key : `{missionId}/logo.png` - - Path stocké : `missions/{missionId}/logo.png` - -2. **Attachments** (si présents, en parallèle) - - Bucket : `missions` - - Key : `{missionId}/attachments/{filename}` - - Path stocké : `missions/{missionId}/attachments/{filename}` - -3. **Vérifications** (après uploads) - - `HeadObjectCommand` pour chaque fichier - - Vérifie l'existence avant N8N - -**Total** : 1 + N uploads + (1 + N) vérifications - ---- - -## 🔄 Résumé du Workflow N8N - -### Données envoyées à N8N : - -```typescript -{ - name: "Mission Example", - oddScope: ["odd-3"], - niveau: "a", - intention: "Description...", - missionType: "remote", - donneurDOrdre: "individual", - projection: "short", - services: ["Gite", "ArtLab"], - participation: "volontaire", - profils: ["DataIntelligence", "Expression"], - guardians: { - "gardien-temps": "user-1", - "gardien-parole": "user-2", - "gardien-memoire": "user-3" - }, - volunteers: ["user-4", "user-5"], - creatorId: "user-creator", - logoPath: "missions/abc-123/logo.png", - config: { - N8N_API_KEY: "...", - MISSION_API_URL: "https://api.slm-lab.net/api" - } -} -``` - -### Actions N8N (workflow interne) : - -1. Création projet Leantime -2. Création collection Outline -3. Création canal RocketChat -4. Création repository Gitea -5. Création projet Penpot - -**Note** : Les IDs retournés ne sont **PAS** sauvegardés en base actuellement. - ---- - -## ⚠️ Points d'Attention et TODOs - -### 1. Sauvegarde des IDs N8N -**Problème** : Les IDs retournés par N8N (leantimeProjectId, etc.) ne sont pas sauvegardés en base. - -**Solution proposée** : -```typescript -// Après le workflow N8N -if (workflowResult.success && workflowResult.results) { - await prisma.mission.update({ - where: { id: mission.id }, - data: { - leantimeProjectId: workflowResult.results.leantimeProjectId, - outlineCollectionId: workflowResult.results.outlineCollectionId, - rocketChatChannelId: workflowResult.results.rocketChatChannelId, - giteaRepositoryUrl: workflowResult.results.giteaRepositoryUrl, - penpotProjectId: workflowResult.results.penpotProjectId - } - }); -} -``` - -### 2. Rollback Complet -**Problème** : En cas d'erreur, la mission reste en base (orphan). - -**Solution proposée** : -```typescript -catch (error) { - // Suppression de la mission et des relations - await prisma.mission.delete({ where: { id: mission.id } }); - // Les MissionUsers et Attachments seront supprimés en cascade - // Nettoyage Minio... -} -``` - -### 3. Credentials Minio Hardcodés -**Problème** : Les credentials Minio sont hardcodés dans `lib/mission-uploads.ts`. - -**Solution** : Déplacer vers variables d'environnement. - -### 4. Gestion des Erreurs N8N Partielles -**Problème** : Si certains services N8N échouent, on continue quand même. - -**Solution** : Décider si on continue ou on rollback selon la criticité. - ---- - -## 📈 Performance et Optimisations - -### Optimisations actuelles : -- ✅ `createMany()` pour MissionUsers (1 requête au lieu de N) -- ✅ `Promise.all()` pour les attachments (parallèle) -- ✅ `HeadObjectCommand` pour vérification (léger) - -### Optimisations possibles : -- 🔄 Transaction Prisma pour atomicité -- 🔄 Batch upload Minio (si supporté) -- 🔄 Retry logic pour N8N -- 🔄 Cache des vérifications Minio - ---- - -## 🔍 Debugging et Logs - -### Points de logging importants : - -1. **Début du workflow** : `=== Mission Creation Started ===` -2. **Création Prisma** : `Mission created successfully` -3. **Upload Minio** : `Logo upload successful` / `Attachment upload successful` -4. **Vérification** : `verifyFileExists()` logs -5. **N8N** : `=== Starting N8N Workflow ===` / `N8N Workflow Result` -6. **Erreurs** : Tous les catch blocks loggent les erreurs - -### Commandes de debug : - -```bash -# Voir les logs du serveur -npm run dev # Logs dans la console - -# Vérifier Minio -# Accéder à https://dome-api.slm-lab.net -# Bucket: missions - -# Vérifier Prisma -npx prisma studio # Interface graphique -``` - ---- - -**Document généré le** : $(date) -**Version** : 1.0 -**Auteur** : Analyse complète du codebase - diff --git a/MISSION_DELETION_IDS_EMPTY_ANALYSIS.md b/MISSION_DELETION_IDS_EMPTY_ANALYSIS.md deleted file mode 100644 index 6bf6b769..00000000 --- a/MISSION_DELETION_IDS_EMPTY_ANALYSIS.md +++ /dev/null @@ -1,377 +0,0 @@ -# Analyse : IDs Vides lors de la Suppression de Mission - -## 🔍 Problème - -Lors de la suppression d'une mission, N8N reçoit des IDs vides : - -```json -{ - "repoName": "", - "leantimeProjectId": 0, - "documentationCollectionId": "", - "rocketchatChannelId": "", - "giteaRepositoryUrl": null, - "outlineCollectionId": null, - "rocketChatChannelId": null -} -``` - -**Cela signifie que les IDs ne sont PAS sauvegardés en base lors de la création.** - ---- - -## 🔄 Flow de Création (Théorique) - -``` -1. POST /api/missions - ↓ -2. Crée mission en Prisma (sans IDs) - ↓ -3. Upload logo dans Minio - ↓ -4. POST N8N webhook (mission-created) - ↓ -5. N8N crée intégrations : - - Gitea → retourne html_url - - Leantime → retourne projectId - - Outline → retourne collectionId - - RocketChat → retourne channelId - ↓ -6. N8N → POST /mission-created (avec les IDs) - ↓ -7. Backend sauvegarde les IDs en base ✅ -``` - ---- - -## ❌ Problèmes Possibles - -### Problème 1: N8N n'appelle pas `/mission-created` - -**Symptôme** : Les IDs ne sont jamais sauvegardés - -**Vérification** : -- Vérifier les logs N8N : le node "Save Mission To API" est-il exécuté ? -- Vérifier les logs backend : y a-t-il des appels à `/mission-created` ? -- Vérifier les erreurs N8N : le workflow s'arrête-t-il avant "Save Mission To API" ? - -**Solution** : -- Vérifier que le node "Save Mission To API" est bien connecté dans le workflow -- Vérifier que l'URL est correcte : `{{ MISSION_API_URL }}/mission-created` -- Vérifier que l'API key est correcte dans les headers - ---- - -### Problème 2: N8N appelle `/mission-created` mais sans `missionId` - -**Symptôme** : L'endpoint ne trouve pas la mission - -**Vérification** : -- Vérifier les logs backend : - ``` - === Mission Created Webhook Received === - Looking up mission by ID: ... - Mission not found: ... - ``` -- Vérifier le body reçu par `/mission-created` : contient-il `missionId` ? - -**Solution** : -- Modifier le workflow N8N pour inclure `missionId` dans le body : - ```json - { - "missionId": "={{ $node['Process Mission Data'].json.missionId }}", - ... - } - ``` - ---- - -### Problème 3: N8N appelle `/mission-created` mais les IDs sont vides - -**Symptôme** : L'endpoint trouve la mission mais les IDs sont `null` ou vides - -**Vérification** : -- Vérifier les logs backend : - ``` - Received mission-created data: { - missionId: "...", - gitRepoUrl: null, // ❌ Vide - leantimeProjectId: null, // ❌ Vide - ... - } - ``` -- Vérifier les logs N8N : les nodes de création retournent-ils bien les IDs ? - -**Solution** : -- Vérifier que les nodes N8N (Create Git Repository, Create Leantime Project, etc.) retournent bien les IDs -- Vérifier que le node "Combine Results" combine correctement les IDs -- Vérifier que le node "Save Mission To API" utilise les bons chemins pour les IDs - ---- - -### Problème 4: Mapping incorrect des champs - -**Symptôme** : Les IDs sont envoyés mais avec des noms incorrects - -**Vérification** : -- Vérifier le body envoyé par N8N : - ```json - { - "gitRepoUrl": "...", // ✅ Correct - "leantimeProjectId": "...", // ✅ Correct - "documentationCollectionId": "...", // ✅ Correct - "rocketchatChannelId": "..." // ✅ Correct - } - ``` -- Vérifier le mapping dans `/mission-created` : - - `gitRepoUrl` → `giteaRepositoryUrl` ✅ - - `documentationCollectionId` → `outlineCollectionId` ✅ - - `rocketchatChannelId` → `rocketChatChannelId` ✅ - -**Solution** : -- Vérifier que les noms de champs correspondent exactement - ---- - -### Problème 5: API Key incorrecte - -**Symptôme** : L'endpoint retourne 401 Unauthorized - -**Vérification** : -- Vérifier les logs backend : - ``` - Invalid API key: { received: '...', expected: '...' } - ``` -- Vérifier que `N8N_API_KEY` est bien configuré dans l'environnement -- Vérifier que N8N envoie bien `x-api-key` dans les headers - -**Solution** : -- Vérifier la variable d'environnement `N8N_API_KEY` -- Vérifier que N8N utilise la bonne API key dans le header - ---- - -## 🔍 Points de Vérification - -### 1. Vérifier les Logs Backend - -**Lors de la création** : -``` -=== Starting N8N Workflow === -Sending to N8N: { missionId: "...", ... } -N8N Workflow Result: { success: true, ... } -``` - -**Lors de l'appel `/mission-created`** : -``` -=== Mission Created Webhook Received === -Received mission-created data: { ... } -Looking up mission by ID: ... -Found mission: { id: "...", ... } -Mission updated successfully: { ... } -``` - -**Si ces logs n'apparaissent pas** → N8N n'appelle pas `/mission-created` - ---- - -### 2. Vérifier les Logs N8N - -**Dans le workflow N8N** : -- Le node "Save Mission To API" est-il exécuté ? -- Y a-t-il des erreurs dans ce node ? -- Le body envoyé contient-il les IDs ? - -**Vérifier le body du node "Save Mission To API"** : -```json -{ - "missionId": "={{ $node['Process Mission Data'].json.missionId }}", - "gitRepoUrl": "={{ $node['Combine Results'].json.gitRepo?.html_url }}", - "leantimeProjectId": "={{ $node['Combine Results'].json.leantimeProject?.result?.[0] }}", - "documentationCollectionId": "={{ $node['Combine Results'].json.docCollection?.data?.id }}", - "rocketchatChannelId": "={{ $node['Combine Results'].json.rocketChatChannel?.channel?._id }}" -} -``` - ---- - -### 3. Vérifier la Base de Données - -**Requête SQL** : -```sql -SELECT - id, - name, - giteaRepositoryUrl, - leantimeProjectId, - outlineCollectionId, - rocketChatChannelId, - createdAt, - updatedAt -FROM Mission -WHERE name = 'Creation' -ORDER BY createdAt DESC -LIMIT 1; -``` - -**Résultat attendu** : -``` -id: cd0225cf-8dfd-4bf0-a20a-6aa9c04ebb42 -name: Creation -giteaRepositoryUrl: https://gite.slm-lab.net/alma/creation ✅ -leantimeProjectId: 123 ✅ -outlineCollectionId: collection-456 ✅ -rocketChatChannelId: channel-789 ✅ -``` - -**Si tous les IDs sont `null`** → Ils ne sont pas sauvegardés - ---- - -## 📋 Checklist de Diagnostic - -### Étape 1: Vérifier que N8N reçoit missionId - -- [ ] Les logs backend montrent `missionId` dans `n8nData` -- [ ] N8N reçoit bien `missionId` dans le webhook - -### Étape 2: Vérifier que N8N crée les intégrations - -- [ ] Les logs N8N montrent que les nodes de création sont exécutés -- [ ] Les nodes retournent bien les IDs (html_url, projectId, etc.) - -### Étape 3: Vérifier que N8N combine les résultats - -- [ ] Le node "Combine Results" contient les IDs -- [ ] Les IDs sont accessibles via les chemins corrects - -### Étape 4: Vérifier que N8N appelle `/mission-created` - -- [ ] Les logs backend montrent des appels à `/mission-created` -- [ ] Le node "Save Mission To API" est exécuté dans N8N -- [ ] Pas d'erreur 401 (API key) ou 404 (mission not found) - -### Étape 5: Vérifier que les IDs sont sauvegardés - -- [ ] Les logs backend montrent "Mission updated successfully" -- [ ] La base de données contient les IDs après création -- [ ] Les IDs sont correctement mappés - ---- - -## 🎯 Actions Recommandées (Sans Toucher au Code) - -### 1. Vérifier les Logs Backend - -```bash -# Chercher les appels à /mission-created -grep "Mission Created Webhook Received" logs.txt - -# Chercher les erreurs -grep "Mission not found" logs.txt -grep "Invalid API key" logs.txt -``` - -### 2. Vérifier le Workflow N8N - -1. Ouvrir le workflow `NeahMissionCreate` -2. Vérifier que le node "Save Mission To API" : - - Est bien connecté après "Combine Results" - - Contient `missionId` dans le body - - Utilise les bons chemins pour les IDs - - A l'URL correcte : `{{ MISSION_API_URL }}/mission-created` - - A l'API key correcte dans les headers - -### 3. Tester Manuellement - -**Appel manuel à `/mission-created`** : -```bash -curl -X POST https://hub.slm-lab.net/api/missions/mission-created \ - -H "Content-Type: application/json" \ - -H "x-api-key: YOUR_API_KEY" \ - -d '{ - "missionId": "cd0225cf-8dfd-4bf0-a20a-6aa9c04ebb42", - "name": "Creation", - "creatorId": "user-id", - "gitRepoUrl": "https://gite.slm-lab.net/alma/creation", - "leantimeProjectId": "123", - "documentationCollectionId": "collection-456", - "rocketchatChannelId": "channel-789" - }' -``` - -**Vérifier la réponse** : -- 200 OK → L'endpoint fonctionne -- 401 Unauthorized → Problème d'API key -- 404 Not Found → Problème de recherche de mission -- 400 Bad Request → Problème de validation - -### 4. Vérifier la Base de Données - -```sql --- Vérifier une mission spécifique -SELECT * FROM Mission WHERE id = 'cd0225cf-8dfd-4bf0-a20a-6aa9c04ebb42'; - --- Vérifier les missions récentes sans IDs -SELECT id, name, createdAt -FROM Mission -WHERE giteaRepositoryUrl IS NULL - AND leantimeProjectId IS NULL - AND outlineCollectionId IS NULL - AND rocketChatChannelId IS NULL -ORDER BY createdAt DESC -LIMIT 10; -``` - ---- - -## 🔧 Solutions Probables - -### Solution 1: Ajouter missionId dans N8N - -**Dans le node "Save Mission To API"** : -```json -{ - "missionId": "={{ $node['Process Mission Data'].json.missionId }}", - ... -} -``` - -### Solution 2: Vérifier les Chemins des IDs dans N8N - -**Vérifier que les chemins sont corrects** : -- `gitRepo.html_url` (pas `gitRepo.body.html_url`) -- `leantimeProject.result[0]` (array avec index 0) -- `docCollection.data.id` (pas `docCollection.id`) -- `rocketChatChannel.channel._id` (pas `rocketChatChannel._id`) - -### Solution 3: Vérifier l'API Key - -**Vérifier que** : -- `N8N_API_KEY` est configuré dans `.env` -- N8N utilise la même clé dans le header `x-api-key` - ---- - -## 📝 Résumé - -**Le problème est que les IDs ne sont pas sauvegardés en base.** - -**Causes possibles** : -1. ❌ N8N n'appelle pas `/mission-created` -2. ❌ N8N appelle mais sans `missionId` → mission non trouvée -3. ❌ N8N appelle mais les IDs sont vides dans le body -4. ❌ API key incorrecte → 401 Unauthorized -5. ❌ Mapping incorrect des champs - -**Action immédiate** : -1. Vérifier les logs backend pour voir si `/mission-created` est appelé -2. Vérifier le workflow N8N pour voir si `missionId` est inclus -3. Vérifier la base de données pour voir si les IDs sont sauvegardés - ---- - -**Date**: $(date) -**Status**: Analyse sans modification de code -**Action Requise**: Vérification des logs et du workflow N8N - diff --git a/MISSION_DELETION_WORKFLOW.md b/MISSION_DELETION_WORKFLOW.md deleted file mode 100644 index 14c88f35..00000000 --- a/MISSION_DELETION_WORKFLOW.md +++ /dev/null @@ -1,625 +0,0 @@ -# Workflow Détaillé : Suppression de Mission - Centrale (/missions) - -## 📋 Vue d'Ensemble - -Ce document trace **chaque étape** du workflow de suppression d'une mission depuis la page Centrale (`/missions/[missionId]`), incluant les vérifications de permissions, la suppression des fichiers Minio, et la suppression en base de données. - ---- - -## 🔄 Flux Complet - Vue d'Ensemble - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. FRONTEND - MissionDetailPage │ -│ - Clic sur bouton "Supprimer" │ -│ - Confirmation utilisateur (confirm dialog) │ -│ - DELETE /api/missions/[missionId] │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 2. BACKEND - DELETE /api/missions/[missionId] │ -│ ├─ Authentification (NextAuth) │ -│ ├─ Vérification mission existe │ -│ ├─ Vérification permissions (créateur ou admin) │ -│ ├─ STEP 1: Suppression logo Minio (si existe) │ -│ ├─ STEP 2: Rollback N8N (TODO - non implémenté) │ -│ └─ STEP 3: Suppression mission Prisma (CASCADE) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 3. PRISMA CASCADE │ -│ - Suppression automatique MissionUsers │ -│ - Suppression automatique Attachments │ -│ ⚠️ Fichiers Minio des attachments NON supprimés │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 📝 ÉTAPE 1 : Frontend - Clic sur Supprimer - -### Fichier : `app/missions/[missionId]/page.tsx` - -### 1.1 Bouton Supprimer - -**Lignes 397-411** - -```typescript - -``` - -**Caractéristiques** : -- ✅ Bouton rouge avec icône Trash2 -- ✅ Désactivé pendant la suppression (`deleting` state) -- ✅ Affiche un spinner pendant le traitement - -### 1.2 Handler de Suppression - -**Lignes 143-176** - -```typescript -const handleDeleteMission = async () => { - // 1. Confirmation utilisateur - if (!confirm("Êtes-vous sûr de vouloir supprimer cette mission ? Cette action est irréversible.")) { - return; // Annulation si l'utilisateur refuse - } - - try { - setDeleting(true); // Active le spinner - - // 2. Appel API DELETE - const response = await fetch(`/api/missions/${missionId}`, { - method: 'DELETE', - }); - - // 3. Vérification de la réponse - if (!response.ok) { - throw new Error('Failed to delete mission'); - } - - // 4. Notification de succès - toast({ - title: "Mission supprimée", - description: "La mission a été supprimée avec succès", - }); - - // 5. Redirection vers la liste des missions - router.push('/missions'); - - } catch (error) { - console.error('Error deleting mission:', error); - toast({ - title: "Erreur", - description: "Impossible de supprimer la mission", - variant: "destructive", - }); - } finally { - setDeleting(false); // Désactive le spinner - } -}; -``` - -**Points importants** : -- ✅ **Double confirmation** : Dialog natif du navigateur -- ✅ **Gestion d'état** : `deleting` pour le spinner -- ✅ **Redirection automatique** vers `/missions` en cas de succès -- ✅ **Gestion d'erreurs** avec toast notification - ---- - -## 🗄️ ÉTAPE 2 : Backend - DELETE /api/missions/[missionId] - -### Fichier : `app/api/missions/[missionId]/route.ts` - -### 2.1 Authentification et Récupération de la Mission - -**Lignes 292-316** - -```typescript -export async function DELETE( - request: Request, - props: { params: Promise<{ missionId: string }> } -) { - const params = await props.params; - - try { - // 1. Vérification de l'authentification - const session = await getServerSession(authOptions); - if (!session?.user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - // 2. Récupération de la mission avec relations - const mission = await prisma.mission.findUnique({ - where: { id: params.missionId }, - include: { - missionUsers: { - include: { - user: true // Inclut les infos des utilisateurs - } - } - } - }); - - // 3. Vérification que la mission existe - if (!mission) { - return NextResponse.json({ error: 'Mission not found' }, { status: 404 }); - } -``` - -**Points importants** : -- ✅ Vérification de session NextAuth -- ✅ Récupération de la mission avec `missionUsers` (pour logging/info) -- ✅ Retour 404 si mission n'existe pas - -### 2.2 Vérification des Permissions - -**Lignes 318-324** - -```typescript -// Vérification : utilisateur doit être créateur OU admin -const isCreator = mission.creatorId === session.user.id; -const userRoles = Array.isArray(session.user.role) ? session.user.role : []; -const isAdmin = userRoles.includes('admin') || userRoles.includes('ADMIN'); - -if (!isCreator && !isAdmin) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); -} -``` - -**Règles de permissions** : -- ✅ **Créateur** : Peut supprimer sa propre mission -- ✅ **Admin** : Peut supprimer n'importe quelle mission -- ❌ **Autres utilisateurs** : Même gardiens/volontaires ne peuvent pas supprimer - -**Points importants** : -- ✅ Vérification stricte : seul le créateur ou un admin peut supprimer -- ✅ Les gardiens (même gardien-memoire) ne peuvent pas supprimer -- ✅ Retour 403 Forbidden si pas autorisé - -### 2.3 STEP 1 : Suppression du Logo dans Minio - -**Lignes 326-334** - -```typescript -// Suppression du logo si présent -if (mission.logo) { - try { - await deleteMissionLogo(params.missionId, mission.logo); - } catch (error) { - console.error('Error deleting mission logo:', error); - // Continue deletion even if logo deletion fails - } -} -``` - -**Fonction `deleteMissionLogo()` - `lib/mission-uploads.ts`** : - -**Lignes 42-61** - -```typescript -export async function deleteMissionLogo( - missionId: string, - logoPath: string -): Promise { - try { - const normalizedPath = ensureMissionsPrefix(logoPath); - // ⚠️ TODO: La fonction ne fait que logger, ne supprime pas vraiment ! - console.log('Deleting mission logo:', { - missionId, - originalPath: logoPath, - normalizedPath - }); - // TODO: Implémenter la suppression réelle avec DeleteObjectCommand - } catch (error) { - console.error('Error deleting mission logo:', error); - throw error; - } -} -``` - -**⚠️ PROBLÈME IDENTIFIÉ** : -- ❌ La fonction `deleteMissionLogo()` ne supprime **PAS** réellement le fichier -- ❌ Elle ne fait que logger les informations -- ⚠️ Le logo reste dans Minio après suppression de la mission - -**Solution proposée** : -```typescript -export async function deleteMissionLogo( - missionId: string, - logoPath: string -): Promise { - try { - const { DeleteObjectCommand } = await import('@aws-sdk/client-s3'); - const normalizedPath = ensureMissionsPrefix(logoPath); - const minioPath = normalizedPath.replace(/^missions\//, ''); - - await s3Client.send(new DeleteObjectCommand({ - Bucket: 'missions', - Key: minioPath - })); - - console.log('Mission logo deleted successfully:', minioPath); - } catch (error) { - console.error('Error deleting mission logo:', error); - throw error; - } -} -``` - -**Points importants** : -- ✅ Continue la suppression même si le logo échoue (try/catch) -- ⚠️ **BUG** : Le logo n'est pas réellement supprimé actuellement - -### 2.4 STEP 2 : Rollback N8N (TODO - Non Implémenté) - -**Lignes 336-344** - -```typescript -// Trigger n8n workflow for rollback -// TODO: Implement rollbackMission method in N8nService -// const n8nService = new N8nService(); -// try { -// await n8nService.rollbackMission(mission); -// } catch (error) { -// console.error('Error during mission rollback:', error); -// // Continue with mission deletion even if rollback fails -// } -``` - -**⚠️ NON IMPLÉMENTÉ** : -- ❌ Le rollback N8N n'est pas appelé -- ❌ Les intégrations externes (Leantime, Outline, RocketChat, etc.) ne sont **PAS** supprimées -- ⚠️ Les ressources externes restent orphelines - -**Méthode disponible mais non utilisée** : -- ✅ `N8nService.triggerMissionRollback()` existe dans `lib/services/n8n-service.ts` -- ✅ Webhook URL : `https://brain.slm-lab.net/webhook/mission-rollback` -- ❌ Mais n'est pas appelée dans le DELETE - -**Solution proposée** : -```typescript -// Rollback N8N -const n8nService = new N8nService(); -try { - await n8nService.triggerMissionRollback({ - missionId: mission.id, - leantimeProjectId: mission.leantimeProjectId, - outlineCollectionId: mission.outlineCollectionId, - rocketChatChannelId: mission.rocketChatChannelId, - giteaRepositoryUrl: mission.giteaRepositoryUrl, - penpotProjectId: mission.penpotProjectId - }); -} catch (error) { - console.error('Error during mission rollback:', error); - // Continue with mission deletion even if rollback fails -} -``` - -### 2.5 STEP 3 : Suppression de la Mission en Base de Données - -**Lignes 346-349** - -```typescript -// Suppression de la mission (CASCADE automatique) -await prisma.mission.delete({ - where: { id: params.missionId } -}); -``` - -**Schéma Prisma - Relations avec CASCADE** : - -```prisma -model Mission { - // ... - attachments Attachment[] - missionUsers MissionUser[] -} - -model Attachment { - mission Mission @relation(fields: [missionId], references: [id], onDelete: Cascade) - // ... -} - -model MissionUser { - mission Mission @relation(fields: [missionId], references: [id], onDelete: Cascade) - // ... -} -``` - -**CASCADE automatique** : -- ✅ **MissionUsers** : Supprimés automatiquement (`onDelete: Cascade`) -- ✅ **Attachments** : Supprimés automatiquement (`onDelete: Cascade`) -- ❌ **Fichiers Minio** : **NON supprimés automatiquement** (pas de trigger) - -**Points importants** : -- ✅ Une seule requête Prisma supprime la mission et toutes ses relations -- ✅ Atomicité : Si la suppression échoue, rien n'est supprimé -- ⚠️ **PROBLÈME** : Les fichiers Minio des attachments ne sont pas supprimés - -### 2.6 Retour de Succès - -**Lignes 351-358** - -```typescript -return NextResponse.json({ success: true }); - -} catch (error) { - console.error('Error deleting mission:', error); - return NextResponse.json( - { error: 'Failed to delete mission' }, - { status: 500 } - ); -} -``` - ---- - -## 🔄 ÉTAPE 3 : Cascade Prisma - -### Suppression Automatique des Relations - -Quand `prisma.mission.delete()` est exécuté, Prisma supprime automatiquement : - -1. **Tous les MissionUsers** associés - ```sql - DELETE FROM "MissionUser" WHERE "missionId" = 'abc-123'; - ``` - -2. **Tous les Attachments** associés - ```sql - DELETE FROM "Attachment" WHERE "missionId" = 'abc-123'; - ``` - -**⚠️ PROBLÈME MAJEUR** : -- ❌ Les fichiers Minio des attachments ne sont **PAS** supprimés -- ❌ Les fichiers restent dans Minio : `missions/{missionId}/attachments/*` -- ⚠️ **Orphelins** : Fichiers sans enregistrement en base - ---- - -## 🧹 Problèmes Identifiés et Solutions - -### Problème 1 : Logo non supprimé dans Minio - -**Symptôme** : Le logo reste dans Minio après suppression - -**Cause** : `deleteMissionLogo()` ne fait que logger, ne supprime pas - -**Solution** : -```typescript -export async function deleteMissionLogo( - missionId: string, - logoPath: string -): Promise { - const { DeleteObjectCommand } = await import('@aws-sdk/client-s3'); - const normalizedPath = ensureMissionsPrefix(logoPath); - const minioPath = normalizedPath.replace(/^missions\//, ''); - - await s3Client.send(new DeleteObjectCommand({ - Bucket: 'missions', - Key: minioPath - })); -} -``` - -### Problème 2 : Attachments non supprimés dans Minio - -**Symptôme** : Les fichiers attachments restent dans Minio - -**Cause** : Pas de suppression des fichiers avant la suppression Prisma - -**Solution** : -```typescript -// Avant la suppression Prisma -if (mission.attachments && mission.attachments.length > 0) { - // Récupérer les attachments - const attachments = await prisma.attachment.findMany({ - where: { missionId: params.missionId } - }); - - // Supprimer chaque fichier Minio - for (const attachment of attachments) { - try { - await deleteMissionAttachment(attachment.filePath); - } catch (error) { - console.error('Error deleting attachment file:', error); - // Continue même si un fichier échoue - } - } -} -``` - -### Problème 3 : Rollback N8N non implémenté - -**Symptôme** : Les intégrations externes restent orphelines - -**Cause** : Code commenté, non implémenté - -**Solution** : -```typescript -// Décommenter et implémenter -const n8nService = new N8nService(); -try { - await n8nService.triggerMissionRollback({ - missionId: mission.id, - leantimeProjectId: mission.leantimeProjectId, - outlineCollectionId: mission.outlineCollectionId, - rocketChatChannelId: mission.rocketChatChannelId, - giteaRepositoryUrl: mission.giteaRepositoryUrl, - penpotProjectId: mission.penpotProjectId - }); -} catch (error) { - console.error('Error during mission rollback:', error); - // Continue avec la suppression même si rollback échoue -} -``` - ---- - -## 📊 Résumé des Opérations - -### Opérations Effectuées ✅ - -1. ✅ **Vérification authentification** : Session NextAuth -2. ✅ **Vérification permissions** : Créateur ou Admin -3. ✅ **Suppression Prisma Mission** : Avec cascade automatique -4. ✅ **Suppression Prisma MissionUsers** : Cascade automatique -5. ✅ **Suppression Prisma Attachments** : Cascade automatique - -### Opérations NON Effectuées ❌ - -1. ❌ **Suppression logo Minio** : Fonction ne fait que logger -2. ❌ **Suppression attachments Minio** : Pas de code pour supprimer -3. ❌ **Rollback N8N** : Code commenté, non implémenté - ---- - -## 🔍 Workflow Complet Corrigé (Proposé) - -```typescript -export async function DELETE(...) { - // 1. Authentification - const session = await getServerSession(authOptions); - if (!session?.user) return 401; - - // 2. Récupération mission avec attachments - const mission = await prisma.mission.findUnique({ - where: { id: params.missionId }, - include: { - attachments: true, // Inclure les attachments - missionUsers: true - } - }); - - if (!mission) return 404; - - // 3. Vérification permissions - const isCreator = mission.creatorId === session.user.id; - const isAdmin = session.user.role?.includes('admin'); - if (!isCreator && !isAdmin) return 403; - - // 4. Suppression logo Minio - if (mission.logo) { - try { - await deleteMissionLogo(mission.id, mission.logo); // À implémenter - } catch (error) { - console.error('Error deleting logo:', error); - // Continue - } - } - - // 5. Suppression attachments Minio - if (mission.attachments && mission.attachments.length > 0) { - for (const attachment of mission.attachments) { - try { - await deleteMissionAttachment(attachment.filePath); - } catch (error) { - console.error('Error deleting attachment:', error); - // Continue - } - } - } - - // 6. Rollback N8N - const n8nService = new N8nService(); - try { - await n8nService.triggerMissionRollback({ - missionId: mission.id, - leantimeProjectId: mission.leantimeProjectId, - outlineCollectionId: mission.outlineCollectionId, - rocketChatChannelId: mission.rocketChatChannelId, - giteaRepositoryUrl: mission.giteaRepositoryUrl, - penpotProjectId: mission.penpotProjectId - }); - } catch (error) { - console.error('Error during N8N rollback:', error); - // Continue même si rollback échoue - } - - // 7. Suppression Prisma (CASCADE) - await prisma.mission.delete({ - where: { id: params.missionId } - }); - - return NextResponse.json({ success: true }); -} -``` - ---- - -## 📈 Ordre d'Exécution Recommandé - -1. **Vérifications** (authentification, permissions, existence) -2. **Suppression fichiers Minio** (logo + attachments) -3. **Rollback N8N** (intégrations externes) -4. **Suppression Prisma** (mission + cascade automatique) - -**Pourquoi cet ordre ?** -- ✅ Supprimer les fichiers avant la base pour éviter les orphelins -- ✅ Rollback N8N avant suppression Prisma pour avoir les IDs -- ✅ Suppression Prisma en dernier (point de non-retour) - ---- - -## ⚠️ Points d'Attention - -### 1. Atomicité -**Problème** : Si une étape échoue, les précédentes sont déjà faites - -**Solution** : Transaction Prisma + Rollback manuel si erreur - -### 2. Performance -**Problème** : Suppression séquentielle des fichiers Minio - -**Solution** : `Promise.all()` pour suppressions parallèles - -### 3. Gestion d'Erreurs -**Problème** : Continue même si certaines suppressions échouent - -**Solution** : Décider si on continue ou on rollback selon criticité - ---- - -## 🔍 Debugging - -### Logs à surveiller : - -1. **Début suppression** : `DELETE /api/missions/[id]` -2. **Permissions** : `isCreator` / `isAdmin` -3. **Suppression logo** : `Deleting mission logo` -4. **Suppression attachments** : `Deleting mission attachment` -5. **Rollback N8N** : `Triggering n8n rollback workflow` -6. **Suppression Prisma** : `prisma.mission.delete()` - -### Vérifications manuelles : - -```bash -# Vérifier Minio -# Accéder à https://dome-api.slm-lab.net -# Bucket: missions -# Vérifier que les dossiers {missionId} sont supprimés - -# Vérifier Prisma -npx prisma studio -# Vérifier que la mission et ses relations sont supprimées -``` - ---- - -**Document généré le** : $(date) -**Version** : 1.0 -**Auteur** : Analyse complète du codebase - diff --git a/MISSION_INTEGRATION_IDS_FIX.md b/MISSION_INTEGRATION_IDS_FIX.md deleted file mode 100644 index 0d26fb56..00000000 --- a/MISSION_INTEGRATION_IDS_FIX.md +++ /dev/null @@ -1,308 +0,0 @@ -# Fix : Sauvegarde des IDs d'Intégration N8N - -## 🔍 Problème Identifié - -Lors de la suppression d'une mission, le webhook N8N reçoit des IDs vides : - -```json -{ - "repoName": "", - "leantimeProjectId": 0, - "documentationCollectionId": "", - "rocketchatChannelId": "" -} -``` - -**Cause** : Les IDs retournés par N8N lors de la création des intégrations n'étaient **jamais sauvegardés en base**. - -### Workflow Problématique - -``` -1. Frontend → POST /api/missions - ↓ -2. Backend crée mission en Prisma - ↓ -3. Backend upload fichiers Minio - ↓ -4. Backend → POST N8N webhook (mission-created) - ↓ -5. N8N crée intégrations (Gitea, Leantime, Outline, RocketChat) - ↓ -6. N8N → POST /mission-created ❌ ENDPOINT N'EXISTAIT PAS - ↓ -7. IDs jamais sauvegardés ❌ -``` - -### Conséquence - -Lors de la suppression : -- Les IDs sont `null` en base -- On envoie des valeurs vides à N8N -- N8N ne peut pas supprimer/fermer les intégrations -- Les ressources externes restent orphelines - ---- - -## ✅ Solution Implémentée - -### 1. Endpoint `/mission-created` Créé - -**Fichier** : `app/api/missions/mission-created/route.ts` - -**Fonctionnalités** : -- ✅ Reçoit les IDs des intégrations créées par N8N -- ✅ Vérifie l'API key (`x-api-key` header) -- ✅ Trouve la mission par `name` + `creatorId` -- ✅ Met à jour la mission avec les IDs -- ✅ Mappe correctement les champs : - - `gitRepoUrl` → `giteaRepositoryUrl` - - `documentationCollectionId` → `outlineCollectionId` - - `rocketchatChannelId` → `rocketChatChannelId` - - `leantimeProjectId` → `leantimeProjectId` (converti en string) - -### 2. Format des Données - -**N8N envoie** : -```json -{ - "name": "Mission Example", - "creatorId": "user-id", - "gitRepoUrl": "https://gite.slm-lab.net/alma/mission-example", - "leantimeProjectId": "123", - "documentationCollectionId": "collection-456", - "rocketchatChannelId": "channel-789" -} -``` - -**Endpoint sauvegarde** : -```typescript -{ - giteaRepositoryUrl: "https://gite.slm-lab.net/alma/mission-example", - leantimeProjectId: "123", - outlineCollectionId: "collection-456", - rocketChatChannelId: "channel-789" -} -``` - -### 3. Sécurité - -- ✅ Vérification de l'API key (`x-api-key` header) -- ✅ Validation des champs requis (`name`, `creatorId`) -- ✅ Gestion d'erreurs complète -- ✅ Logging détaillé pour debugging - ---- - -## 🔄 Nouveau Workflow - -``` -1. Frontend → POST /api/missions - ↓ -2. Backend crée mission en Prisma - ↓ -3. Backend upload fichiers Minio - ↓ -4. Backend → POST N8N webhook (mission-created) - ↓ -5. N8N crée intégrations (Gitea, Leantime, Outline, RocketChat) - ↓ -6. N8N → POST /mission-created ✅ ENDPOINT EXISTE MAINTENANT - ↓ -7. Backend sauvegarde les IDs ✅ - ↓ -8. Mission complète avec tous les IDs ✅ -``` - -**Lors de la suppression** : -``` -1. Frontend → DELETE /api/missions/[id] - ↓ -2. Backend récupère mission (avec IDs sauvegardés) - ↓ -3. Backend extrait/mappe les données - ↓ -4. Backend → POST N8N webhook (mission-delete) - ↓ -5. N8N reçoit les IDs ✅ - ↓ -6. N8N supprime/ferme les intégrations ✅ -``` - ---- - -## 📋 Format de Requête - -### POST /api/missions/mission-created - -**Headers** : -``` -Content-Type: application/json -x-api-key: {N8N_API_KEY} -Authorization: Bearer {keycloak_token} (optionnel) -``` - -**Body** : -```json -{ - "name": "Mission Example", - "creatorId": "user-uuid", - "gitRepoUrl": "https://gite.slm-lab.net/alma/mission-example", - "leantimeProjectId": "123", - "documentationCollectionId": "collection-456", - "rocketchatChannelId": "channel-789", - "niveau": "default", - "intention": "...", - "description": "...", - "donneurDOrdre": "...", - "projection": "...", - "missionType": "remote" -} -``` - -**Réponse Succès** (200) : -```json -{ - "success": true, - "message": "Mission updated successfully", - "mission": { - "id": "mission-uuid", - "name": "Mission Example", - "giteaRepositoryUrl": "https://gite.slm-lab.net/alma/mission-example", - "leantimeProjectId": "123", - "outlineCollectionId": "collection-456", - "rocketChatChannelId": "channel-789" - } -} -``` - -**Réponse Erreur** (400/404/500) : -```json -{ - "error": "Error message", - "details": "Detailed error information" -} -``` - ---- - -## ⚠️ Missions Existantes - -**Problème** : Les missions créées avant cette correction n'ont pas leurs IDs sauvegardés. - -**Solutions possibles** : - -### Option 1 : Migration Manuelle -Pour chaque mission existante, récupérer les IDs depuis les services externes et les mettre à jour manuellement. - -### Option 2 : Script de Migration -Créer un script qui : -1. Liste toutes les missions sans IDs -2. Interroge les services externes (si possible) -3. Met à jour les missions - -### Option 3 : Re-création -Supprimer et recréer les missions (si acceptable). - -**Recommandation** : Option 1 pour les missions critiques, Option 2 pour un grand nombre. - ---- - -## 🧪 Tests - -### Test 1 : Création de Mission - -1. Créer une nouvelle mission via le frontend -2. Vérifier que N8N appelle `/mission-created` -3. Vérifier que la mission en base a les IDs sauvegardés : - ```sql - SELECT id, name, giteaRepositoryUrl, leantimeProjectId, - outlineCollectionId, rocketChatChannelId - FROM Mission - WHERE name = 'Mission Test'; - ``` - -### Test 2 : Suppression de Mission - -1. Supprimer une mission avec IDs sauvegardés -2. Vérifier que N8N reçoit les IDs : - ```json - { - "repoName": "mission-example", - "leantimeProjectId": 123, - "documentationCollectionId": "collection-456", - "rocketchatChannelId": "channel-789" - } - ``` -3. Vérifier que N8N supprime/ferme les intégrations - -### Test 3 : API Key - -1. Appeler `/mission-created` sans `x-api-key` → 401 -2. Appeler avec mauvais `x-api-key` → 401 -3. Appeler avec bon `x-api-key` → 200 - ---- - -## 📝 Logs à Surveiller - -### Création - -``` -=== Mission Created Webhook Received === -Received mission-created data: { ... } -Found mission: { id: "...", name: "..." } -Updating giteaRepositoryUrl: ... -Updating leantimeProjectId: ... -Mission updated successfully: { ... } -``` - -### Suppression - -``` -=== Starting N8N Deletion Workflow === -Extracted repo name from URL: { url: "...", repoName: "..." } -Sending deletion data to N8N: { ... } -N8N Deletion Workflow Result: { success: true, ... } -``` - ---- - -## 🔧 Configuration Requise - -### Variables d'Environnement - -```env -N8N_API_KEY=your-api-key-here -NEXT_PUBLIC_API_URL=https://hub.slm-lab.net -``` - -### N8N Workflow - -Le workflow N8N doit appeler : -- **URL** : `{{ MISSION_API_URL }}/mission-created` -- **Méthode** : POST -- **Headers** : - - `Content-Type: application/json` - - `x-api-key: {{ N8N_API_KEY }}` - - `Authorization: Bearer {{ Keycloak Token }}` (optionnel) - ---- - -## ✅ Checklist - -- [x] Endpoint `/mission-created` créé -- [x] Vérification API key implémentée -- [x] Mapping des champs correct -- [x] Gestion d'erreurs complète -- [x] Logging détaillé -- [ ] Tests manuels effectués -- [ ] Migration des missions existantes (si nécessaire) -- [ ] Documentation N8N mise à jour - ---- - -**Date de correction** : $(date) -**Version** : 1.0 -**Fichiers modifiés** : -- `app/api/missions/mission-created/route.ts` (nouveau) - diff --git a/MISSION_INTEGRATION_IDS_ISSUE_FIX.md b/MISSION_INTEGRATION_IDS_ISSUE_FIX.md deleted file mode 100644 index 4233dc4e..00000000 --- a/MISSION_INTEGRATION_IDS_ISSUE_FIX.md +++ /dev/null @@ -1,253 +0,0 @@ -# Fix: IDs d'Intégration Vides lors de la Suppression - -## 🔍 Problème Identifié - -Lors de la suppression d'une mission, N8N reçoit des IDs vides : - -```json -{ - "missionId": "cd0225cf-8dfd-4bf0-a20a-6aa9c04ebb42", - "name": "Creation", - "repoName": "", - "leantimeProjectId": 0, - "documentationCollectionId": "", - "rocketchatChannelId": "", - "giteaRepositoryUrl": null, - "outlineCollectionId": null, - "rocketChatChannelId": null -} -``` - -**Cause** : Les IDs retournés par N8N lors de la création ne sont **pas sauvegardés en base**. - ---- - -## 🔍 Analyse du Problème - -### Flow Actuel - -``` -1. POST /api/missions → Crée mission en Prisma -2. Upload logo dans Minio -3. POST N8N webhook → N8N crée intégrations -4. N8N → POST /mission-created (avec IDs) -5. ❌ Endpoint cherche mission par name + creatorId (peut échouer) -6. ❌ IDs jamais sauvegardés -7. ❌ Lors de suppression → IDs vides -``` - -### Problèmes Identifiés - -1. **Recherche de mission fragile** : L'endpoint `/mission-created` cherche par `name` + `creatorId`, ce qui peut échouer si : - - Plusieurs missions ont le même nom - - Le nom a changé - - Le creatorId ne correspond pas exactement - -2. **missionId non envoyé** : On n'envoie pas le `missionId` à N8N, donc N8N ne peut pas le renvoyer - -3. **N8N ne renvoie peut-être pas missionId** : Même si on l'envoie, N8N doit le renvoyer dans `/mission-created` - ---- - -## ✅ Solutions Implémentées - -### 1. Envoyer missionId à N8N - -**Fichier** : `app/api/missions/route.ts` - -```typescript -const n8nData = { - ...body, - missionId: mission.id, // ✅ Send missionId so N8N can return it - creatorId: userId, - logoPath: logoPath, - logoUrl: logoUrl, - config: { ... } -}; -``` - -**Avantage** : N8N peut maintenant renvoyer le `missionId` dans `/mission-created` - -### 2. Améliorer la Recherche de Mission - -**Fichier** : `app/api/missions/mission-created/route.ts` - -```typescript -// Prefer missionId if provided, otherwise use name + creatorId -let mission; - -if (body.missionId) { - // ✅ Use missionId if provided (more reliable) - mission = await prisma.mission.findUnique({ - where: { id: body.missionId } - }); -} else if (body.name && body.creatorId) { - // Fallback to name + creatorId (for backward compatibility) - mission = await prisma.mission.findFirst({ - where: { - name: body.name, - creatorId: body.creatorId - }, - orderBy: { createdAt: 'desc' } - }); -} -``` - -**Avantages** : -- ✅ Recherche par `missionId` (plus fiable) -- ✅ Fallback vers `name` + `creatorId` (rétrocompatibilité) -- ✅ Gestion d'erreurs améliorée - ---- - -## 📋 Format de Requête N8N → /mission-created - -### Format Recommandé (avec missionId) - -```json -{ - "missionId": "cd0225cf-8dfd-4bf0-a20a-6aa9c04ebb42", - "name": "Creation", - "creatorId": "user-id", - "gitRepoUrl": "https://gite.slm-lab.net/alma/creation", - "leantimeProjectId": "123", - "documentationCollectionId": "collection-456", - "rocketchatChannelId": "channel-789" -} -``` - -### Format de Fallback (sans missionId) - -```json -{ - "name": "Creation", - "creatorId": "user-id", - "gitRepoUrl": "https://gite.slm-lab.net/alma/creation", - "leantimeProjectId": "123", - "documentationCollectionId": "collection-456", - "rocketchatChannelId": "channel-789" -} -``` - ---- - -## 🔧 Action Requise dans N8N - -### Modifier le Node "Save Mission To API" - -Le node N8N doit inclure `missionId` dans le body : - -**Avant** : -```json -{ - "name": "{{ name }}", - "creatorId": "{{ creatorId }}", - "gitRepoUrl": "{{ gitRepo.html_url }}", - ... -} -``` - -**Après** : -```json -{ - "missionId": "{{ missionId }}", // ✅ Ajouter missionId - "name": "{{ name }}", - "creatorId": "{{ creatorId }}", - "gitRepoUrl": "{{ gitRepo.html_url }}", - "leantimeProjectId": "{{ leantimeProject.result[0] }}", - "documentationCollectionId": "{{ docCollection.data.id }}", - "rocketchatChannelId": "{{ rocketChatChannel.channel._id }}", - ... -} -``` - -**Où trouver missionId dans N8N** : -- Il est dans les données initiales : `{{ $node['Process Mission Data'].json.missionId }}` -- Ou dans le body original : `{{ $json.missionId }}` - ---- - -## 🧪 Tests - -### Test 1: Vérifier missionId est envoyé à N8N - -1. Créer une mission -2. Vérifier les logs : - ``` - Sending to N8N: { missionId: "...", ... } - ``` -3. ✅ `missionId` doit être présent - -### Test 2: Vérifier N8N renvoie missionId - -1. Vérifier les logs N8N -2. Vérifier que le node "Save Mission To API" inclut `missionId` -3. ✅ `missionId` doit être dans le body envoyé à `/mission-created` - -### Test 3: Vérifier IDs sont sauvegardés - -1. Créer une mission -2. Vérifier les logs : - ``` - === Mission Created Webhook Received === - Looking up mission by ID: ... - Mission updated successfully: { ... } - ``` -3. Vérifier en base : - ```sql - SELECT id, name, giteaRepositoryUrl, leantimeProjectId, - outlineCollectionId, rocketChatChannelId - FROM Mission - WHERE id = '...'; - ``` -4. ✅ Les IDs doivent être présents - -### Test 4: Vérifier Suppression - -1. Supprimer une mission avec IDs sauvegardés -2. Vérifier les logs : - ``` - Sending deletion data to N8N: { - repoName: "creation", - leantimeProjectId: 123, - ... - } - ``` -3. ✅ Les IDs doivent être présents (pas vides) - ---- - -## 📝 Checklist - -- [x] Envoyer `missionId` à N8N lors de la création -- [x] Améliorer recherche de mission dans `/mission-created` -- [ ] **Modifier N8N workflow pour inclure `missionId` dans `/mission-created`** -- [ ] Tester création avec `missionId` -- [ ] Tester sauvegarde des IDs -- [ ] Tester suppression avec IDs sauvegardés - ---- - -## ⚠️ Action Immédiate Requise - -**Modifier le workflow N8N** pour inclure `missionId` dans le node "Save Mission To API" : - -1. Ouvrir le workflow N8N `NeahMissionCreate` -2. Trouver le node "Save Mission To API" -3. Ajouter `missionId` dans le body : - ```json - { - "missionId": "={{ $node['Process Mission Data'].json.missionId }}", - ... - } - ``` -4. Sauvegarder et activer le workflow - ---- - -**Date**: $(date) -**Version**: 1.1 -**Fichiers Modifiés**: -- `app/api/missions/route.ts` (ajout missionId dans n8nData) -- `app/api/missions/mission-created/route.ts` (recherche par missionId) - diff --git a/N8N_COMPLETE_WORKFLOW_MAPPING.md b/N8N_COMPLETE_WORKFLOW_MAPPING.md deleted file mode 100644 index cfddacb6..00000000 --- a/N8N_COMPLETE_WORKFLOW_MAPPING.md +++ /dev/null @@ -1,673 +0,0 @@ -# Mapping Complet N8N - Création et Suppression de Mission - -## 📋 Vue d'Ensemble - -Ce document décrit le mapping complet entre notre API et les workflows N8N pour la création et la suppression de missions, basé sur les workflows réels partagés. - ---- - -## 🔄 Workflow de Création - NeahMissionCreate - -### Structure du Workflow - -``` -Webhook (mission-created) - ↓ -Process Mission Data - ↓ -Get Keycloak Token - ↓ -Process Token - ↓ -Debug Service Data - ↓ -Merge Paths - ↓ -IF Run Integrations - ├─ IF Needs Git Repository - │ ├─ Create Git Repository (si Gite ou Calcul) - │ ├─ Create Readme - │ └─ Git Wiki - ├─ Create Documentation Collection - ├─ Create Leantime Project - │ └─ Leantime Avatar - └─ Create RocketChat Channel - ↓ -Combine Results - ↓ -Save Mission To API (POST /mission-created) - ↓ -Process Results - ↓ -Respond To Webhook -``` - -### Données Envoyées par Notre API → N8N - -**Endpoint** : `POST https://brain.slm-lab.net/webhook/mission-created` - -**Format** : -```typescript -{ - name: string, - oddScope: string[], - niveau: string, - intention: string, - missionType: string, - donneurDOrdre: string, - projection: string, - services: string[], - participation: string, - profils: string[], - guardians: { - "gardien-temps": userId, - "gardien-parole": userId, - "gardien-memoire": userId - }, - volunteers: string[], - creatorId: string, - logo: { - data: "data:image/png;base64,...", - name: string, - type: string - }, - attachments: Array<{ - data: "data:...;base64,...", - name: string, - type: string - }>, - config: { - N8N_API_KEY: string, - MISSION_API_URL: string - } -} -``` - -### Traitement N8N - Process Mission Data - -Le node "Process Mission Data" transforme les données en : - -```javascript -{ - missionOriginal: { ... }, // Données originales - missionProcessed: { - name: "Mission Example", - sanitizedName: "mission-example", // Nom nettoyé pour URLs - intention: "...", - description: "...", - startDate: "2024-01-01", - endDate: "2024-01-31", - missionType: "remote", - guardians: { ... }, - volunteers: [ ... ], - profils: [ ... ], - services: ["Gite", "ArtLab"], // Détermine quelles intégrations créer - clientId: 2, - rocketChatUsernames: [userId1, userId2, ...], // Gardiens + volontaires - logo: { data: "...", name: "...", type: "..." }, - attachments: [ ... ] - }, - config: { - GITEA_API_URL: "https://gite.slm-lab.net/api/v1", - GITEA_API_TOKEN: "...", - GITEA_OWNER: "alma", - LEANTIME_API_URL: "https://agilite.slm-lab.net", - LEANTIME_API_TOKEN: "...", - ROCKETCHAT_API_URL: "https://parole.slm-lab.net/", - ROCKETCHAT_AUTH_TOKEN: "...", - ROCKETCHAT_USER_ID: "...", - OUTLINE_API_URL: "https://chapitre.slm-lab.net/api", - OUTLINE_API_TOKEN: "...", - MISSION_API_URL: "https://hub.slm-lab.net", - // ... autres configs - }, - creatorId: "user-id" -} -``` - -### Intégrations Créées par N8N - -#### 1. Gitea Repository (Conditionnel) - -**Condition** : `services.includes('Gite') || services.includes('Calcul')` - -**Node** : "Create Git Repository" -- **Méthode** : POST -- **URL** : `{{ GITEA_API_URL }}/user/repos` -- **Body** : - ```json - { - "name": "{{ sanitizedName }}", - "private": true, - "auto_init": true, - "avatar_url": "{{ logo.data }}" - } - ``` -- **Résultat** : `{ html_url: "https://gite.slm-lab.net/alma/mission-example" }` - -**Actions supplémentaires** : -- Create Readme : Crée un document README dans Outline -- Git Wiki : Configure le wiki externe du repo vers Outline - -#### 2. Leantime Project - -**Node** : "Create Leantime Project" -- **Méthode** : POST -- **URL** : `{{ LEANTIME_API_URL }}/api/jsonrpc` -- **Body** : - ```json - { - "method": "leantime.rpc.Projects.Projects.addProject", - "jsonrpc": "2.0", - "id": 1, - "params": { - "values": { - "name": "{{ name }}", - "clientId": {{ clientId }}, - "details": "{{ intention }}", - "type": "project", - "start": "{{ startDate }}", - "end": "{{ endDate }}", - "status": "open", - "psettings": "restricted", - "avatar": "{{ logo.data }}" - } - } - } - ``` -- **Résultat** : `{ result: [projectId] }` (array avec 1 élément) - -**Action supplémentaire** : -- Leantime Avatar : Met à jour l'avatar du projet - -#### 3. Outline Collection - -**Node** : "Create Documentation Collection" -- **Méthode** : POST -- **URL** : `{{ OUTLINE_API_URL }}/api/collections.create` -- **Body** : - ```json - { - "name": "{{ sanitizedName }}", - "description": "{{ description }}", - "permission": "read", - "private": true - } - ``` -- **Résultat** : `{ data: { id: "collection-id", url: "/collection/..." } }` - -#### 4. RocketChat Channel - -**Node** : "Create RocketChat Channel" -- **Méthode** : POST -- **URL** : `{{ ROCKETCHAT_API_URL }}/api/v1/channels.create` -- **Body** : - ```json - { - "name": "{{ sanitizedName }}", - "members": [{{ rocketChatUsernames }}], - "readOnly": false, - "avatarUrl": "{{ logo.data }}" - } - ``` -- **Résultat** : `{ channel: { _id: "channel-id", ... } }` - -### Save Mission To API - Retour vers Notre API - -**Node** : "Save Mission To API" -- **Méthode** : POST -- **URL** : `{{ MISSION_API_URL }}/mission-created` -- **Headers** : - - `Content-Type: application/json` - - `Authorization: Bearer {{ Keycloak Token }}` - - `x-api-key: {{ N8N_API_KEY }}` -- **Body** : - ```json - { - "name": "{{ name }}", - "niveau": "{{ niveau }}", - "intention": "{{ intention }}", - "description": "{{ description }}", - "gitRepoUrl": "{{ gitRepo.html_url }}", - "leantimeProjectId": "{{ leantimeProject.result[0] }}", - "documentationCollectionId": "{{ docCollection.data.id }}", - "rocketchatChannelId": "{{ rocketChatChannel.channel._id }}", - "donneurDOrdre": "{{ donneurDOrdre }}", - "projection": "{{ projection }}", - "missionType": "{{ missionType }}", - "creatorId": "{{ creatorId }}" - } - ``` - -**⚠️ IMPORTANT** : Cet endpoint `/mission-created` n'existe **PAS** actuellement dans notre codebase. Il devrait : -1. Recevoir les IDs des intégrations créées -2. Mettre à jour la mission en base avec ces IDs -3. Mapper les champs : - - `gitRepoUrl` → `giteaRepositoryUrl` - - `documentationCollectionId` → `outlineCollectionId` - - `rocketchatChannelId` → `rocketChatChannelId` - ---- - -## 🗑️ Workflow de Suppression - NeahMissionDelete_Pro - -### Structure du Workflow - -``` -Webhook Delete (mission-delete) - ↓ -Process Delete Data - ↓ -Get Keycloak Token - ↓ -[En parallèle] - ├─ Delete Gitea Repo - ├─ Close Leantime Project - ├─ Delete Outline Collection - └─ Close RocketChat Channel - ↓ -Combine Results - ↓ -Save Deletion To API (POST /mission-deleted) -``` - -### Données Envoyées par Notre API → N8N - -**Endpoint** : `POST https://brain.slm-lab.net/webhook-test/mission-delete` - -**Format** : -```typescript -{ - missionId: string, - name: string, - repoName: string, // ✅ Extrait de giteaRepositoryUrl - leantimeProjectId: number | 0, // ✅ Converti en number - documentationCollectionId: string, // ✅ Mappé depuis outlineCollectionId - rocketchatChannelId: string, // ✅ Mappé depuis rocketChatChannelId - // Champs originaux pour référence - giteaRepositoryUrl: string | null, - outlineCollectionId: string | null, - rocketChatChannelId: string | null, - penpotProjectId: string | null, - config: { - N8N_API_KEY: string, - MISSION_API_URL: string - } -} -``` - -### Traitement N8N - Process Delete Data - -Le node "Process Delete Data" transforme les données en : - -```javascript -{ - missionData: { - repoName: input.repoName || '', - leantimeId: input.leantimeProjectId || 0, - collectionId: input.documentationCollectionId || '', - rocketChatRoomId: input.rocketchatChannelId || '' - }, - config: { - GITEA_API_URL: "https://gite.slm-lab.net/api/v1", - GITEA_API_TOKEN: "...", - GITEA_OWNER: "alma", - LEANTIME_API_URL: "https://agilite.slm-lab.net", - LEANTIME_API_TOKEN: "...", - ROCKETCHAT_API_URL: "https://parole.slm-lab.net/", - ROCKETCHAT_AUTH_TOKEN: "...", - ROCKETCHAT_USER_ID: "...", - OUTLINE_API_URL: "https://chapitre.slm-lab.net/api", - OUTLINE_API_TOKEN: "...", - MISSION_API_URL: "https://hub.slm-lab.net", - KEYCLOAK_BASE_URL: "https://connect.slm-lab.net", - KEYCLOAK_REALM: "cercle", - KEYCLOAK_CLIENT_ID: "lab", - KEYCLOAK_CLIENT_SECRET: "..." - } -} -``` - -### Actions de Suppression N8N - -#### 1. Delete Gitea Repo - -**Node** : "Delete Gitea Repo" -- **Méthode** : DELETE -- **URL** : `{{ GITEA_API_URL }}/repos/{{ GITEA_OWNER }}/{{ repoName }}` -- **Headers** : `Authorization: token {{ GITEA_API_TOKEN }}` -- **ContinueOnFail** : `true` -- **Résultat attendu** : Status 204 = succès - -#### 2. Close Leantime Project - -**Node** : "Close Leantime Project" -- **Méthode** : POST -- **URL** : `{{ LEANTIME_API_URL }}/api/jsonrpc` -- **Body** : - ```json - { - "method": "leantime.rpc.Projects.Projects.patch", - "jsonrpc": "2.0", - "id": 1, - "params": { - "id": {{ leantimeId }}, - "params": { "status": "closed" } - } - } - ``` -- **ContinueOnFail** : `true` -- **Note** : Le projet est **fermé** (status: "closed"), pas supprimé - -#### 3. Delete Outline Collection - -**Node** : "Delete Outline Collection" -- **Méthode** : POST -- **URL** : `{{ OUTLINE_API_URL }}/api/collections.delete` -- **Body** : `{ "id": "{{ collectionId }}" }` -- **ContinueOnFail** : `true` -- **Résultat attendu** : Status 200 = succès - -#### 4. Close RocketChat Channel - -**Node** : "Close RocketChat Channel" -- **Méthode** : POST -- **URL** : `{{ ROCKETCHAT_API_URL }}/api/v1/channels.close` -- **Body** : `{ "roomId": "{{ rocketChatRoomId }}" }` -- **ContinueOnFail** : `true` -- **Note** : Le canal est **fermé**, pas supprimé - -### Combine Results - -Le node "Combine Results" combine les résultats : - -```javascript -{ - status: "deleted", - timestamp: "2024-01-01T12:00:00.000Z", - details: { - gitea: true || "already_deleted", - leantime: true || false, - outline: true || false, - rocketchat: true || false - } -} -``` - -### Save Deletion To API - Retour vers Notre API - -**Node** : "Save Deletion To API" -- **Méthode** : POST -- **URL** : `{{ MISSION_API_URL }}/mission-deleted` -- **Headers** : - - `Authorization: Bearer {{ Keycloak Token }}` -- **Body** : - ```json - { - "status": "archived", - "results": { - "gitea": true, - "leantime": true, - "outline": true, - "rocketchat": true - } - } - ``` - -**⚠️ IMPORTANT** : Cet endpoint `/mission-deleted` n'existe **PAS** actuellement dans notre codebase. Il pourrait servir à : -1. Confirmer la suppression -2. Logger les résultats -3. Nettoyer des données supplémentaires si nécessaire - ---- - -## 📊 Mapping Complet des Champs - -### Création (Notre API → N8N → Retour) - -| Notre Base | Envoyé à N8N | N8N Crée | Retour N8N | Stocké en Base | -|-----------|--------------|----------|------------|----------------| -| - | `name` | - | `name` | `name` | -| - | `services` | Détermine intégrations | - | `services` | -| - | `logo.data` | Avatar/Logo | - | `logo` (path) | -| - | - | Gitea Repo | `gitRepoUrl` | `giteaRepositoryUrl` | -| - | - | Leantime Project | `leantimeProjectId` | `leantimeProjectId` | -| - | - | Outline Collection | `documentationCollectionId` | `outlineCollectionId` | -| - | - | RocketChat Channel | `rocketchatChannelId` | `rocketChatChannelId` | - -### Suppression (Notre Base → N8N) - -| Notre Base | Extrait/Transformé | Envoyé à N8N | N8N Attend | -|-----------|-------------------|--------------|------------| -| `giteaRepositoryUrl` | Extraction nom | `repoName` | `repoName` | -| `leantimeProjectId` | Converti en number | `leantimeProjectId` | `leantimeId` | -| `outlineCollectionId` | Direct | `documentationCollectionId` | `collectionId` | -| `rocketChatChannelId` | Direct | `rocketchatChannelId` | `rocketChatRoomId` | - ---- - -## 🔧 Transformations Clés - -### 1. Extraction du Nom du Repository Gitea - -**Problème** : Notre base stocke l'URL complète, N8N attend le nom seul - -**Solution** : -```typescript -// Format: https://gite.slm-lab.net/alma/mission-example -// ou: https://gite.slm-lab.net/api/v1/repos/alma/mission-example - -let repoName = ''; -if (giteaRepositoryUrl) { - try { - const url = new URL(giteaRepositoryUrl); - const pathParts = url.pathname.split('/').filter(Boolean); - repoName = pathParts[pathParts.length - 1] || ''; - } catch (error) { - const match = giteaRepositoryUrl.match(/\/([^\/]+)\/?$/); - repoName = match ? match[1] : ''; - } -} -``` - -### 2. Mapping des Champs - -**Création** : -- N8N retourne `gitRepoUrl` → On stocke `giteaRepositoryUrl` -- N8N retourne `documentationCollectionId` → On stocke `outlineCollectionId` -- N8N retourne `rocketchatChannelId` → On stocke `rocketChatChannelId` - -**Suppression** : -- On stocke `giteaRepositoryUrl` → On envoie `repoName` (extrait) -- On stocke `outlineCollectionId` → On envoie `documentationCollectionId` -- On stocke `rocketChatChannelId` → On envoie `rocketchatChannelId` - -### 3. Conversion de Types - -**Leantime Project ID** : -- Stocké en base : `string | null` -- Envoyé à N8N : `number | 0` (converti) -- N8N attend : `number` (dans `leantimeId`) - ---- - -## ⚠️ Endpoints Manquants - -### 1. POST /mission-created - -**Rôle** : Recevoir les IDs des intégrations créées par N8N - -**Format attendu** : -```typescript -POST /mission-created -Headers: { - Authorization: "Bearer {keycloak_token}", - x-api-key: "{N8N_API_KEY}" -} -Body: { - name: string, - niveau: string, - intention: string, - description: string, - gitRepoUrl: string, // À mapper vers giteaRepositoryUrl - leantimeProjectId: string, // À mapper vers leantimeProjectId - documentationCollectionId: string, // À mapper vers outlineCollectionId - rocketchatChannelId: string, // À mapper vers rocketChatChannelId - donneurDOrdre: string, - projection: string, - missionType: string, - creatorId: string -} -``` - -**Action requise** : -1. Trouver la mission par `name` + `creatorId` -2. Mettre à jour avec les IDs retournés -3. Mapper les champs correctement - -### 2. POST /mission-deleted - -**Rôle** : Confirmer la suppression (optionnel) - -**Format attendu** : -```typescript -POST /mission-deleted -Headers: { - Authorization: "Bearer {keycloak_token}" -} -Body: { - status: "archived", - results: { - gitea: boolean, - leantime: boolean, - outline: boolean, - rocketchat: boolean - } -} -``` - -**Action requise** : -- Logger les résultats -- Potentiellement nettoyer des données supplémentaires - ---- - -## 🔄 Flow Complet - Vue d'Ensemble - -### Création - -``` -1. Frontend → POST /api/missions - ↓ -2. Backend crée mission en Prisma - ↓ -3. Backend upload fichiers Minio - ↓ -4. Backend → POST N8N webhook (mission-created) - ↓ -5. N8N crée intégrations (Gitea, Leantime, Outline, RocketChat) - ↓ -6. N8N → POST /mission-created (⚠️ endpoint manquant) - ↓ -7. Backend met à jour mission avec IDs (⚠️ non implémenté) -``` - -### Suppression - -``` -1. Frontend → DELETE /api/missions/[id] - ↓ -2. Backend récupère mission - ↓ -3. Backend extrait/mappe les données - ↓ -4. Backend → POST N8N webhook (mission-delete) - ↓ -5. N8N supprime/ferme intégrations - ↓ -6. N8N → POST /mission-deleted (⚠️ endpoint manquant) - ↓ -7. Backend supprime logo Minio - ↓ -8. Backend supprime attachments Minio - ↓ -9. Backend supprime mission Prisma (CASCADE) -``` - ---- - -## 📝 Notes Importantes - -### 1. Noms de Champs Incohérents - -- **Création** : N8N retourne `gitRepoUrl`, `documentationCollectionId`, `rocketchatChannelId` -- **Suppression** : N8N attend `repoName`, `documentationCollectionId`, `rocketchatChannelId` -- **Notre Base** : Stocke `giteaRepositoryUrl`, `outlineCollectionId`, `rocketChatChannelId` - -**Solution** : Mapping cohérent dans les deux sens - -### 2. Endpoint /mission-created Manquant - -Actuellement, les IDs retournés par N8N ne sont **PAS** sauvegardés en base. Il faudrait : -- Créer l'endpoint `/mission-created` -- Trouver la mission (par `name` + `creatorId` ou `missionId`) -- Mettre à jour avec les IDs - -### 3. Services Conditionnels - -- **Gitea** : Créé seulement si `services.includes('Gite') || services.includes('Calcul')` -- **Leantime** : Toujours créé -- **Outline** : Toujours créé -- **RocketChat** : Toujours créé - -### 4. Gestion d'Erreurs - -- Tous les nodes N8N ont `continueOnFail: true` -- Les erreurs sont loggées mais n'arrêtent pas le workflow -- Les résultats indiquent quelles intégrations ont réussi/échoué - ---- - -## 🔍 Points de Debugging - -### Création - -1. **Vérifier données envoyées à N8N** : - ``` - Sending to N8N: { ... } - ``` - -2. **Vérifier réponse N8N** : - ``` - N8N Workflow Result: { success: true, results: {...} } - ``` - -3. **Vérifier endpoint /mission-created** : - - Doit recevoir les IDs - - Doit mettre à jour la mission - -### Suppression - -1. **Vérifier extraction repoName** : - ``` - Extracted repo name from URL: { url: "...", repoName: "..." } - ``` - -2. **Vérifier données envoyées à N8N** : - ``` - Sending deletion data to N8N: { ... } - ``` - -3. **Vérifier réponse N8N** : - ``` - N8N Deletion Workflow Result: { success: true, results: {...} } - ``` - ---- - -**Document généré le** : $(date) -**Version** : 1.0 -**Workflows N8N** : -- NeahMissionCreate (création) -- NeahMissionDelete_Pro (suppression) - diff --git a/N8N_DELETION_WORKFLOW_MAPPING.md b/N8N_DELETION_WORKFLOW_MAPPING.md deleted file mode 100644 index f7c21f2d..00000000 --- a/N8N_DELETION_WORKFLOW_MAPPING.md +++ /dev/null @@ -1,342 +0,0 @@ -# Mapping N8N Workflow - Mission Deletion - -## 📋 Vue d'Ensemble - -Ce document décrit le mapping entre les données de notre API et le format attendu par le workflow N8N `NeahMissionDelete_Pro`. - ---- - -## 🔄 Workflow N8N - Structure - -### Nodes du Workflow - -1. **Webhook Delete** : Reçoit POST sur `/mission-delete` -2. **Process Delete Data** : Transforme les données d'entrée -3. **Get Keycloak Token** : Obtient un token d'authentification -4. **Delete Gitea Repo** : Supprime le repository Gitea (continueOnFail: true) -5. **Close Leantime Project** : Ferme le projet Leantime (continueOnFail: true) -6. **Delete Outline Collection** : Supprime la collection Outline (continueOnFail: true) -7. **Close RocketChat Channel** : Ferme le canal RocketChat (continueOnFail: true) -8. **Combine Results** : Combine les résultats de toutes les suppressions -9. **Save Deletion To API** : Envoie les résultats à l'API - ---- - -## 📊 Mapping des Données - -### Données Envoyées par Notre API - -```typescript -{ - missionId: string, - name: string, - repoName: string, // Extrait de giteaRepositoryUrl - leantimeProjectId: number | null, - documentationCollectionId: string, // Mappé depuis outlineCollectionId - rocketchatChannelId: string, // Mappé depuis rocketChatChannelId - // Champs originaux conservés pour référence - giteaRepositoryUrl: string | null, - outlineCollectionId: string | null, - rocketChatChannelId: string | null, - penpotProjectId: string | null, - config: { - N8N_API_KEY: string, - MISSION_API_URL: string - } -} -``` - -### Données Attendues par N8N (Process Delete Data) - -Le node "Process Delete Data" transforme les données en : - -```javascript -{ - missionData: { - repoName: input.repoName || '', - leantimeId: input.leantimeProjectId || 0, - collectionId: input.documentationCollectionId || '', - rocketChatRoomId: input.rocketchatChannelId || '' - }, - config: { - GITEA_API_URL: "https://gite.slm-lab.net/api/v1", - GITEA_API_TOKEN: "...", - GITEA_OWNER: "alma", - LEANTIME_API_URL: "https://agilite.slm-lab.net", - LEANTIME_API_TOKEN: "...", - ROCKETCHAT_API_URL: "https://parole.slm-lab.net/", - ROCKETCHAT_AUTH_TOKEN: "...", - ROCKETCHAT_USER_ID: "...", - OUTLINE_API_URL: "https://chapitre.slm-lab.net/api", - OUTLINE_API_TOKEN: "...", - MISSION_API_URL: "https://hub.slm-lab.net", - KEYCLOAK_BASE_URL: "https://connect.slm-lab.net", - KEYCLOAK_REALM: "cercle", - KEYCLOAK_CLIENT_ID: "lab", - KEYCLOAK_CLIENT_SECRET: "..." - } -} -``` - ---- - -## 🔧 Transformations Effectuées - -### 1. Extraction du Nom du Repository Gitea - -**Problème** : Notre base stocke `giteaRepositoryUrl` (URL complète), mais N8N attend `repoName` (nom seul) - -**Solution** : Extraction du nom depuis l'URL - -```typescript -// Format possible: -// - https://gite.slm-lab.net/alma/repo-name -// - https://gite.slm-lab.net/api/v1/repos/alma/repo-name - -let repoName = ''; -if (mission.giteaRepositoryUrl) { - try { - const url = new URL(mission.giteaRepositoryUrl); - const pathParts = url.pathname.split('/').filter(Boolean); - repoName = pathParts[pathParts.length - 1] || ''; - } catch (error) { - // Fallback: extraction regex - const match = mission.giteaRepositoryUrl.match(/\/([^\/]+)\/?$/); - repoName = match ? match[1] : ''; - } -} -``` - -**Exemples** : -- `https://gite.slm-lab.net/alma/mission-abc` → `mission-abc` -- `https://gite.slm-lab.net/api/v1/repos/alma/mission-xyz` → `mission-xyz` - -### 2. Mapping des Champs - -| Notre Base de Données | N8N Attend | Transformation | -|----------------------|------------|----------------| -| `giteaRepositoryUrl` | `repoName` | Extraction du nom depuis URL | -| `leantimeProjectId` | `leantimeProjectId` | Direct (converti en number) | -| `outlineCollectionId` | `documentationCollectionId` | Direct mapping | -| `rocketChatChannelId` | `rocketchatChannelId` | Direct mapping (lowercase 'c') | - ---- - -## 🎯 Actions N8N par Service - -### 1. Gitea Repository - -**Node** : "Delete Gitea Repo" -- **Méthode** : DELETE -- **URL** : `{{ GITEA_API_URL }}/repos/{{ GITEA_OWNER }}/{{ repoName }}` -- **Headers** : `Authorization: token {{ GITEA_API_TOKEN }}` -- **ContinueOnFail** : `true` (continue même si échoue) - -**Résultat attendu** : Status 204 (No Content) = succès - -### 2. Leantime Project - -**Node** : "Close Leantime Project" -- **Méthode** : POST -- **URL** : `{{ LEANTIME_API_URL }}/api/jsonrpc` -- **Headers** : `X-API-Key: {{ LEANTIME_API_TOKEN }}` -- **Body** : - ```json - { - "method": "leantime.rpc.Projects.Projects.patch", - "jsonrpc": "2.0", - "id": 1, - "params": { - "id": {{ leantimeId }}, - "params": { "status": "closed" } - } - } - ``` -- **ContinueOnFail** : `true` - -**Note** : Le projet est **fermé** (status: "closed"), pas supprimé - -### 3. Outline Collection - -**Node** : "Delete Outline Collection" -- **Méthode** : POST -- **URL** : `{{ OUTLINE_API_URL }}/api/collections.delete` -- **Headers** : `Authorization: Bearer {{ OUTLINE_API_TOKEN }}` -- **Body** : `{ "id": "{{ collectionId }}" }` -- **ContinueOnFail** : `true` - -**Résultat attendu** : Status 200 = succès - -### 4. RocketChat Channel - -**Node** : "Close RocketChat Channel" -- **Méthode** : POST -- **URL** : `{{ ROCKETCHAT_API_URL }}/api/v1/channels.close` -- **Headers** : - - `X-Auth-Token: {{ ROCKETCHAT_AUTH_TOKEN }}` - - `X-User-Id: {{ ROCKETCHAT_USER_ID }}` -- **Body** : `{ "roomId": "{{ rocketChatRoomId }}" }` -- **ContinueOnFail** : `true` - -**Note** : Le canal est **fermé**, pas supprimé - ---- - -## 📤 Réponse N8N - -### Format de Réponse (Combine Results) - -```javascript -{ - status: "deleted", - timestamp: "2024-01-01T12:00:00.000Z", - details: { - gitea: true || "already_deleted", - leantime: true || false, - outline: true || false, - rocketchat: true || false - } -} -``` - -### Envoi à l'API (Save Deletion To API) - -Le workflow envoie ensuite les résultats à : -- **URL** : `{{ MISSION_API_URL }}/mission-deleted` -- **Méthode** : POST -- **Headers** : `Authorization: Bearer {{ Keycloak Token }}` -- **Body** : - ```json - { - "status": "archived", - "results": { - "gitea": true, - "leantime": true, - "outline": true, - "rocketchat": true - } - } - ``` - ---- - -## ⚠️ Points d'Attention - -### 1. Gestion des Erreurs - -- Tous les nodes de suppression ont `continueOnFail: true` -- Si une suppression échoue, le workflow continue avec les autres -- Les résultats indiquent quelles suppressions ont réussi/échoué - -### 2. Différences de Comportement - -- **Gitea** : Suppression complète du repository -- **Leantime** : Fermeture (status: "closed"), pas suppression -- **Outline** : Suppression complète de la collection -- **RocketChat** : Fermeture du canal, pas suppression - -### 3. Extraction du Repo Name - -- L'extraction doit gérer différents formats d'URL -- Si l'extraction échoue, `repoName` sera vide -- Le workflow N8N gérera le cas où `repoName` est vide - -### 4. Mapping des Champs - -- **documentationCollectionId** : Mappé depuis `outlineCollectionId` -- **rocketchatChannelId** : Mappé depuis `rocketChatChannelId` (attention au 'c' minuscule) -- **leantimeProjectId** : Converti en number (0 si null) - ---- - -## 🔍 Debugging - -### Logs à Surveiller - -1. **Extraction repo name** : - ``` - Extracted repo name from URL: { url: "...", repoName: "..." } - ``` - -2. **Données envoyées à N8N** : - ``` - Sending deletion data to N8N: { ... } - ``` - -3. **Résultat N8N** : - ``` - N8N Deletion Workflow Result: { success: true, results: {...} } - ``` - -### Vérifications - -1. **Repo name extrait correctement** ? - - Vérifier les logs d'extraction - - Format attendu : nom simple sans URL - -2. **Mapping des champs correct** ? - - `documentationCollectionId` = `outlineCollectionId` - - `rocketchatChannelId` = `rocketChatChannelId` - -3. **N8N a reçu les données** ? - - Vérifier les logs N8N - - Vérifier le webhook a été appelé - ---- - -## 📝 Exemple Complet - -### Données en Base - -```typescript -{ - id: "abc-123", - name: "Mission Example", - giteaRepositoryUrl: "https://gite.slm-lab.net/alma/mission-example", - leantimeProjectId: "123", - outlineCollectionId: "collection-456", - rocketChatChannelId: "channel-789" -} -``` - -### Données Envoyées à N8N - -```typescript -{ - missionId: "abc-123", - name: "Mission Example", - repoName: "mission-example", // Extrait de l'URL - leantimeProjectId: 123, // Converti en number - documentationCollectionId: "collection-456", // Mappé - rocketchatChannelId: "channel-789", // Mappé (lowercase 'c') - giteaRepositoryUrl: "https://gite.slm-lab.net/alma/mission-example", - outlineCollectionId: "collection-456", - rocketChatChannelId: "channel-789", - config: { - N8N_API_KEY: "...", - MISSION_API_URL: "https://hub.slm-lab.net" - } -} -``` - -### Données Traitées par N8N - -```javascript -{ - missionData: { - repoName: "mission-example", - leantimeId: 123, - collectionId: "collection-456", - rocketChatRoomId: "channel-789" - }, - config: { ... } -} -``` - ---- - -**Document généré le** : $(date) -**Version** : 1.0 -**Workflow N8N** : NeahMissionDelete_Pro -**Webhook URL** : https://brain.slm-lab.net/webhook-test/mission-delete - diff --git a/NAVBAR_TIME_INTEGRATION.md b/NAVBAR_TIME_INTEGRATION.md deleted file mode 100644 index 3a224ee9..00000000 --- a/NAVBAR_TIME_INTEGRATION.md +++ /dev/null @@ -1,129 +0,0 @@ -# Navigation Bar Time Integration - -## 🎯 Overview - -The navigation bar (`components/main-nav.tsx`) currently displays a static time that doesn't refresh. This document outlines how to integrate it into the unified refresh system. - -## 🔍 Current Issue - -**File**: `components/main-nav.tsx` (lines 228-231) - -```typescript -// Current code - STATIC (doesn't refresh) -const now = new Date(); -const formattedDate = format(now, "d MMMM yyyy", { locale: fr }); -const formattedTime = format(now, "HH:mm"); -``` - -**Problem**: Time is calculated once when component renders and never updates. - -## ✅ Solution - -### Step 1: Create Time Component - -**File**: `components/main-nav-time.tsx` (✅ Already created) - -This component: -- Uses `useState` to track current time -- Uses `useUnifiedRefresh` hook for 1-second updates -- Properly cleans up on unmount -- No API calls needed (client-side only) - -### Step 2: Update MainNav Component - -**File**: `components/main-nav.tsx` - -**Changes needed**: - -1. **Import the new component**: -```typescript -import { MainNavTime } from './main-nav-time'; -``` - -2. **Remove static time code** (lines 228-231): -```typescript -// DELETE THESE LINES: -// Format current date and time -const now = new Date(); -const formattedDate = format(now, "d MMMM yyyy", { locale: fr }); -const formattedTime = format(now, "HH:mm"); -``` - -3. **Replace time display** (lines 294-298): -```typescript -// BEFORE: -{/* Center - Date and Time */} -
-
{formattedDate}
-
{formattedTime}
-
- -// AFTER: -{/* Center - Date and Time */} - -``` - -### Step 3: Verify Integration - -After changes: -- ✅ Time updates every second -- ✅ Uses unified refresh system -- ✅ Proper cleanup on unmount -- ✅ No memory leaks -- ✅ Consistent with other widgets - -## 📊 Benefits - -1. **Real-time clock**: Time updates every second -2. **Unified system**: Uses same refresh manager as widgets -3. **Memory safe**: Proper cleanup prevents leaks -4. **Consistent**: Same pattern as other components -5. **Maintainable**: Centralized refresh logic - -## 🔧 Technical Details - -### Refresh Configuration - -- **Resource**: `navbar-time` -- **Interval**: 1000ms (1 second) -- **Priority**: `high` (real-time display) -- **API Calls**: None (client-side only) -- **Cleanup**: Automatic via `useUnifiedRefresh` - -### Integration with Refresh Manager - -The time component registers with the refresh manager: - -```typescript -useUnifiedRefresh({ - resource: 'navbar-time', - interval: REFRESH_INTERVALS.NAVBAR_TIME, // 1000ms - enabled: true, // Always enabled - onRefresh: async () => { - setCurrentTime(new Date()); - }, - priority: 'high', -}); -``` - -## ✅ Implementation Checklist - -- [x] Create `components/main-nav-time.tsx` -- [x] Add `NAVBAR_TIME` to refresh intervals -- [x] Add `navbar-time` to refreshable resources -- [ ] Update `components/main-nav.tsx` to use new component -- [ ] Test time updates correctly -- [ ] Verify cleanup on unmount -- [ ] Test with multiple tabs - -## 🎯 Expected Result - -After implementation: -- Time updates smoothly every second -- No performance impact -- No memory leaks -- Consistent with unified refresh system - ---- - -*Last Updated: Navbar Time Integration Guide* diff --git a/NGINX_HEADER_SIZE_FIX.md b/NGINX_HEADER_SIZE_FIX.md deleted file mode 100644 index c66d9fe6..00000000 --- a/NGINX_HEADER_SIZE_FIX.md +++ /dev/null @@ -1,156 +0,0 @@ -# Fix Erreur 502 - Headers trop grands (Nginx) - -## 🔍 Problème Identifié - -**Erreur Nginx** : -``` -upstream sent too big header while reading response header from upstream -``` - -**Cause** : Le cookie de session NextAuth est trop grand (> 4KB par défaut dans Nginx). Le JWT contient : -- `accessToken` (Keycloak) - ~1-2KB -- `refreshToken` (Keycloak) - ~1-2KB -- `idToken` (Keycloak) - ~1-2KB -- Données utilisateur (roles, etc.) - ~500B-1KB -- **Total** : ~4-7KB, ce qui dépasse la limite Nginx par défaut - -## ✅ Solutions - -### Solution 1 : Augmenter la limite Nginx (RECOMMANDÉ) - -**Fichier** : Configuration Nginx (généralement `/etc/nginx/sites-available/hub.slm-lab.net` ou similaire) - -**Ajouter dans le bloc `server` ou `location`** : -```nginx -server { - # ... autres configs ... - - # Augmenter la taille maximale des headers - proxy_buffer_size 16k; - proxy_buffers 8 16k; - proxy_busy_buffers_size 32k; - large_client_header_buffers 4 32k; - - # Spécifiquement pour les headers de réponse - proxy_headers_hash_max_size 512; - proxy_headers_hash_bucket_size 128; - - # ... reste de la config ... -} -``` - -**OU** pour une solution plus simple, ajouter seulement : -```nginx -server { - # ... autres configs ... - - # Augmenter la limite des headers - large_client_header_buffers 4 32k; - - # ... reste de la config ... -} -``` - -**Puis redémarrer Nginx** : -```bash -sudo nginx -t # Vérifier la config -sudo systemctl reload nginx # Ou sudo service nginx reload -``` - -### Solution 2 : Réduire la taille du JWT (ALTERNATIVE) - -Si on ne peut pas modifier Nginx, on peut réduire la taille du JWT en ne stockant pas tous les tokens. - -**Modification** : `app/api/auth/options.ts` - -**Option A** : Ne pas stocker `idToken` dans le JWT (si pas nécessaire) -```typescript -// Dans JWT callback -token.idToken = account.id_token ?? ''; // ❌ Supprimer cette ligne -``` - -**Option B** : Stocker seulement les tokens nécessaires -```typescript -// Stocker seulement accessToken et refreshToken -// idToken peut être récupéré depuis Keycloak si nécessaire -``` - -**Note** : Cette solution réduit la fonctionnalité. La Solution 1 est préférable. - -## 🔧 Configuration Nginx Complète Recommandée - -```nginx -server { - listen 443 ssl http2; - server_name hub.slm-lab.net; - - # ... SSL config ... - - # Augmenter les limites pour les gros headers NextAuth - proxy_buffer_size 16k; - proxy_buffers 8 16k; - proxy_busy_buffers_size 32k; - large_client_header_buffers 4 32k; - - # Timeouts - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - - location / { - proxy_pass http://172.16.0.102:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - - # Headers pour NextAuth - proxy_set_header Cookie $http_cookie; - } -} -``` - -## 📊 Vérification - -**Après modification Nginx** : - -1. **Tester la config** : -```bash -sudo nginx -t -``` - -2. **Recharger Nginx** : -```bash -sudo systemctl reload nginx -``` - -3. **Tester la connexion** : -- Se connecter via Keycloak -- Vérifier que l'erreur 502 ne se produit plus -- Vérifier les logs Nginx pour confirmer - -## 🎯 Cause Technique - -NextAuth crée un cookie JWT qui contient : -- Le JWT encrypté avec `NEXTAUTH_SECRET` -- Le JWT contient tous les tokens Keycloak -- La taille totale peut dépasser 4KB - -Nginx a une limite par défaut de 4KB pour les headers de réponse. Quand Next.js essaie de renvoyer un cookie > 4KB, Nginx rejette avec "upstream sent too big header". - -## ✅ Solution Immédiate - -**Action** : Modifier la configuration Nginx pour augmenter `large_client_header_buffers` à au moins `4 32k` ou `8 16k`. - -**Impact** : Résout immédiatement l'erreur 502. - ---- - -**Document créé le** : $(date) -**Priorité** : HAUTE - C'est la cause de l'erreur 502 - - diff --git a/NOTIFICATION_AND_WIDGET_ANALYSIS.md b/NOTIFICATION_AND_WIDGET_ANALYSIS.md deleted file mode 100644 index 86aeefeb..00000000 --- a/NOTIFICATION_AND_WIDGET_ANALYSIS.md +++ /dev/null @@ -1,548 +0,0 @@ -# Notification and Widget Update System - Complete File & Route Analysis - -## 📋 Table of Contents -1. [Notification System](#notification-system) -2. [Widget Update System](#widget-update-system) -3. [API Routes](#api-routes) -4. [Components](#components) -5. [Services & Libraries](#services--libraries) -6. [Hooks](#hooks) -7. [Types](#types) - ---- - -## 🔔 Notification System - -### API Routes - -#### 1. **GET `/api/notifications`** -- **File**: `app/api/notifications/route.ts` -- **Purpose**: Fetch paginated notifications for authenticated user -- **Query Parameters**: - - `page` (default: 1) - - `limit` (default: 20, max: 100) -- **Response**: - ```json - { - "notifications": Notification[], - "page": number, - "limit": number, - "total": number - } - ``` -- **Cache**: 30 seconds client-side cache -- **Authentication**: Required (session-based) - -#### 2. **GET `/api/notifications/count`** -- **File**: `app/api/notifications/count/route.ts` -- **Purpose**: Get notification count (total and unread) for authenticated user -- **Response**: - ```json - { - "total": number, - "unread": number, - "sources": { - [source]: { - "total": number, - "unread": number - } - } - } - ``` -- **Cache**: 10 seconds client-side cache -- **Authentication**: Required - -#### 3. **POST `/api/notifications/[id]/read`** -- **File**: `app/api/notifications/[id]/read/route.ts` -- **Purpose**: Mark a specific notification as read -- **Parameters**: - - `id` (path parameter): Notification ID (format: `source-sourceId`) -- **Response**: - ```json - { - "success": boolean - } - ``` -- **Authentication**: Required - -#### 4. **POST `/api/notifications/read-all`** -- **File**: `app/api/notifications/read-all/route.ts` -- **Purpose**: Mark all notifications as read for authenticated user -- **Response**: - ```json - { - "success": boolean - } - ``` -- **Authentication**: Required - -#### 5. **GET `/api/debug/notifications`** -- **File**: `app/api/debug/notifications/route.ts` -- **Purpose**: Debug endpoint to test notification system -- **Response**: Detailed debug information including: - - Environment variables status - - User information - - Notification service test results - - Performance metrics -- **Authentication**: Required - -### Services - -#### 1. **NotificationService** (Singleton) -- **File**: `lib/services/notifications/notification-service.ts` -- **Purpose**: Core notification aggregation service -- **Features**: - - Multi-source notification aggregation (adapter pattern) - - Redis caching (30s for counts, 5min for lists) - - Background refresh scheduling - - Cache invalidation on read operations - - Lock mechanism to prevent concurrent refreshes -- **Methods**: - - `getInstance()`: Get singleton instance - - `getNotifications(userId, page, limit)`: Fetch notifications - - `getNotificationCount(userId)`: Get notification counts - - `markAsRead(userId, notificationId)`: Mark notification as read - - `markAllAsRead(userId)`: Mark all as read - - `invalidateCache(userId)`: Invalidate user caches - - `scheduleBackgroundRefresh(userId)`: Schedule background refresh - -#### 2. **NotificationAdapter Interface** -- **File**: `lib/services/notifications/notification-adapter.interface.ts` -- **Purpose**: Interface for notification source adapters -- **Methods**: - - `getNotifications(userId, page?, limit?)`: Fetch notifications - - `getNotificationCount(userId)`: Get counts - - `markAsRead(userId, notificationId)`: Mark as read - - `markAllAsRead(userId)`: Mark all as read - - `isConfigured()`: Check if adapter is configured - -#### 3. **LeantimeAdapter** (Implementation) -- **File**: `lib/services/notifications/leantime-adapter.ts` -- **Purpose**: Leantime notification source adapter -- **Features**: - - Fetches notifications from Leantime API via JSON-RPC - - Maps Leantime user IDs by email - - Transforms Leantime notifications to unified format - - Supports marking notifications as read -- **Configuration**: - - `LEANTIME_API_URL` environment variable - - `LEANTIME_TOKEN` environment variable - -### Components - -#### 1. **NotificationBadge** -- **File**: `components/notification-badge.tsx` -- **Purpose**: Notification bell icon with badge and dropdown -- **Features**: - - Displays unread count badge - - Dropdown menu with recent notifications - - Manual refresh button - - Mark as read functionality - - Mark all as read functionality - - Source badges (e.g., "Agilité" for Leantime) - - Links to source systems - - Error handling and retry -- **Used in**: `components/main-nav.tsx` - -#### 2. **MainNav** (Notification Integration) -- **File**: `components/main-nav.tsx` -- **Purpose**: Main navigation bar with notification badge -- **Notification Features**: - - Includes `` component - - Browser notification permission handling - - User status-based notification management - -### Hooks - -#### 1. **useNotifications** -- **File**: `hooks/use-notifications.ts` -- **Purpose**: React hook for notification management -- **Features**: - - Automatic polling (60 seconds interval) - - Rate limiting (5 seconds minimum between fetches) - - Debounced count fetching (300ms) - - Manual refresh support - - Mount/unmount lifecycle management - - Error handling -- **Returns**: - ```typescript - { - notifications: Notification[], - notificationCount: NotificationCount, - loading: boolean, - error: string | null, - fetchNotifications: (page?, limit?) => Promise, - fetchNotificationCount: () => Promise, - markAsRead: (notificationId: string) => Promise, - markAllAsRead: () => Promise - } - ``` - -### Types - -#### 1. **Notification Types** -- **File**: `lib/types/notification.ts` -- **Interfaces**: - - `Notification`: Main notification interface - - `id`: string (format: `source-sourceId`) - - `source`: 'leantime' | 'nextcloud' | 'gitea' | 'dolibarr' | 'moodle' - - `sourceId`: string - - `type`: string - - `title`: string - - `message`: string - - `link?`: string - - `isRead`: boolean - - `timestamp`: Date - - `priority`: 'low' | 'normal' | 'high' - - `user`: { id: string, name?: string } - - `metadata?`: Record - - `NotificationCount`: Count interface - - `total`: number - - `unread`: number - - `sources`: Record - ---- - -## 🎨 Widget Update System - -### Dashboard Widgets - -The main dashboard (`app/page.tsx`) contains the following widgets: - -1. **QuoteCard** - Daily quote widget -2. **Calendar** - Upcoming events widget -3. **News** - News articles widget -4. **Duties** - Tasks/Devoirs widget (Leantime) -5. **Email** - Email inbox widget -6. **Parole** - Chat messages widget (Rocket.Chat) - -### Widget Components & Update Mechanisms - -#### 1. **Calendar Widget** -- **Files**: - - `components/calendar.tsx` (Main dashboard widget) - - `components/calendar-widget.tsx` (Alternative implementation) - - `components/calendar/calendar-widget.tsx` (Calendar-specific widget) -- **Update Mechanism**: - - **Manual Refresh**: Refresh button in header - - **Auto Refresh**: Every 5 minutes (300000ms interval) - - **API Endpoint**: `/api/calendars?refresh=true` - - **Features**: - - Fetches calendars with events - - Filters upcoming events (today and future) - - Sorts by date (oldest first) - - Shows up to 7 events - - Displays calendar color coding -- **State Management**: - - `useState` for events, loading, error - - `useEffect` for initial fetch and interval setup - -#### 2. **News Widget** -- **File**: `components/news.tsx` -- **Update Mechanism**: - - **Manual Refresh**: Refresh button in header - - **Initial Load**: On component mount when authenticated - - **API Endpoint**: `/api/news?limit=100` or `/api/news?refresh=true&limit=100` - - **Features**: - - Fetches up to 100 news articles - - Displays article count - - Click to open in new tab - - Scrollable list (max-height: 400px) -- **State Management**: - - `useState` for news, loading, error, refreshing - - `useEffect` for initial fetch on authentication - -#### 3. **Duties Widget (Tasks)** -- **File**: `components/flow.tsx` -- **Update Mechanism**: - - **Manual Refresh**: Refresh button in header - - **Initial Load**: On component mount - - **API Endpoint**: `/api/leantime/tasks?refresh=true` - - **Features**: - - Fetches tasks from Leantime - - Filters out completed tasks (status 5) - - Sorts by due date (oldest first) - - Shows up to 7 tasks - - Displays task status badges - - Links to Leantime ticket view -- **State Management**: - - `useState` for tasks, loading, error, refreshing - - `useEffect` for initial fetch - -#### 4. **Email Widget** -- **File**: `components/email.tsx` -- **Update Mechanism**: - - **Manual Refresh**: Refresh button in header - - **Initial Load**: On component mount - - **API Endpoint**: `/api/courrier?folder=INBOX&page=1&perPage=5` (+ `&refresh=true` for refresh) - - **Features**: - - Fetches 5 most recent emails from INBOX - - Sorts by date (most recent first) - - Shows read/unread status - - Displays sender, subject, date - - Link to full email view (`/courrier`) -- **State Management**: - - `useState` for emails, loading, error, mailUrl - - `useEffect` for initial fetch - -#### 5. **Parole Widget (Chat Messages)** -- **File**: `components/parole.tsx` -- **Update Mechanism**: - - **Manual Refresh**: Refresh button in header - - **Auto Polling**: Every 30 seconds (30000ms interval) - - **Initial Load**: On authentication - - **API Endpoint**: `/api/rocket-chat/messages` (+ `?refresh=true` for refresh) - - **Features**: - - Fetches recent chat messages from Rocket.Chat - - Displays sender avatar, name, message - - Shows room/channel information - - Click to navigate to full chat (`/parole`) - - Authentication check with sign-in prompt -- **State Management**: - - `useState` for messages, loading, error, refreshing - - `useEffect` for initial fetch and polling setup - - Session status checking - -#### 6. **QuoteCard Widget** -- **File**: `components/quote-card.tsx` -- **Update Mechanism**: (To be verified - likely static or daily update) - -### Widget Update Patterns - -#### Common Update Mechanisms: - -1. **Manual Refresh**: - - All widgets have a refresh button in their header - - Triggers API call with `refresh=true` parameter - - Shows loading/spinning state during refresh - -2. **Auto Refresh/Polling**: - - **Calendar**: 5 minutes interval - - **Parole**: 30 seconds interval - - Others: On component mount only - -3. **Session-Based Loading**: - - Widgets check authentication status - - Only fetch data when `status === 'authenticated'` - - Show loading state during authentication check - -4. **Error Handling**: - - All widgets display error messages - - Retry buttons available - - Graceful degradation (empty states) - -5. **State Management**: - - All widgets use React `useState` hooks - - Loading states managed locally - - Error states managed locally - -### Related API Routes for Widgets - -#### Calendar -- **GET `/api/calendars`**: Fetch calendars with events -- **GET `/api/calendars/[id]/events`**: Fetch events for specific calendar -- **GET `/api/calendars/[id]`**: Get calendar details - -#### News -- **GET `/api/news`**: Fetch news articles - - Query params: `limit`, `refresh` - -#### Tasks (Leantime) -- **GET `/api/leantime/tasks`**: Fetch tasks - - Query params: `refresh` - -#### Email (Courrier) -- **GET `/api/courrier`**: Fetch emails - - Query params: `folder`, `page`, `perPage`, `refresh` -- **POST `/api/courrier/refresh`**: Force refresh email cache - -#### Chat (Rocket.Chat) -- **GET `/api/rocket-chat/messages`**: Fetch messages - - Query params: `refresh` - ---- - -## 📁 Complete File Structure - -### Notification Files - -``` -app/api/notifications/ -├── route.ts # GET /api/notifications -├── count/ -│ └── route.ts # GET /api/notifications/count -├── read-all/ -│ └── route.ts # POST /api/notifications/read-all -└── [id]/ - └── read/ - └── route.ts # POST /api/notifications/[id]/read - -app/api/debug/ -└── notifications/ - └── route.ts # GET /api/debug/notifications - -lib/services/notifications/ -├── notification-service.ts # Core notification service -├── notification-adapter.interface.ts # Adapter interface -└── leantime-adapter.ts # Leantime adapter implementation - -lib/types/ -└── notification.ts # Notification type definitions - -hooks/ -└── use-notifications.ts # React hook for notifications - -components/ -├── notification-badge.tsx # Notification UI component -└── main-nav.tsx # Navigation with notification badge -``` - -### Widget Files - -``` -app/ -└── page.tsx # Main dashboard with widgets - -components/ -├── calendar.tsx # Calendar widget -├── calendar-widget.tsx # Alternative calendar widget -├── calendar/ -│ └── calendar-widget.tsx # Calendar-specific widget -├── news.tsx # News widget -├── flow.tsx # Duties/Tasks widget -├── email.tsx # Email widget -├── parole.tsx # Chat messages widget -└── quote-card.tsx # Quote widget - -app/api/ -├── calendars/ -│ ├── route.ts # GET /api/calendars -│ └── [id]/ -│ └── events/ -│ └── route.ts # GET /api/calendars/[id]/events -├── news/ -│ └── route.ts # GET /api/news -├── leantime/ -│ └── tasks/ -│ └── route.ts # GET /api/leantime/tasks -├── courrier/ -│ ├── route.ts # GET /api/courrier -│ └── refresh/ -│ └── route.ts # POST /api/courrier/refresh -└── rocket-chat/ - └── messages/ - └── route.ts # GET /api/rocket-chat/messages -``` - ---- - -## 🔄 Update Flow Diagrams - -### Notification Update Flow - -``` -User Action / Polling - ↓ -useNotifications Hook - ↓ -API Route (/api/notifications or /api/notifications/count) - ↓ -NotificationService.getInstance() - ↓ -Check Redis Cache - ├─ Cache Hit → Return cached data - └─ Cache Miss → Fetch from Adapters - ↓ - LeantimeAdapter (and other adapters) - ↓ - Transform & Aggregate - ↓ - Store in Redis Cache - ↓ - Return to API - ↓ - Return to Hook - ↓ - Update Component State -``` - -### Widget Update Flow - -``` -Component Mount / User Click Refresh - ↓ -useEffect / onClick Handler - ↓ -fetch() API Call - ├─ With refresh=true (manual) - └─ Without refresh (initial) - ↓ -API Route Handler - ├─ Check Cache (if applicable) - ├─ Fetch from External Service - └─ Return Data - ↓ -Update Component State - ├─ setLoading(false) - ├─ setData(response) - └─ setError(null) - ↓ -Re-render Component -``` - ---- - -## 🎯 Key Features Summary - -### Notification System -- ✅ Multi-source aggregation (adapter pattern) -- ✅ Redis caching with TTL -- ✅ Background refresh scheduling -- ✅ Polling mechanism (60s interval) -- ✅ Rate limiting (5s minimum) -- ✅ Mark as read / Mark all as read -- ✅ Cache invalidation on updates -- ✅ Error handling and retry -- ✅ Source badges and links - -### Widget System -- ✅ Manual refresh buttons -- ✅ Auto-refresh/polling (widget-specific intervals) -- ✅ Session-based loading -- ✅ Error handling -- ✅ Loading states -- ✅ Empty states -- ✅ Responsive design - ---- - -## 📝 Notes - -1. **Notification Sources**: Currently only Leantime adapter is implemented. Other adapters (Nextcloud, Gitea, Dolibarr, Moodle) are commented out in the service. - -2. **Cache Strategy**: - - Notification counts: 30 seconds TTL - - Notification lists: 5 minutes TTL - - Widget data: Varies by widget (some use API-level caching) - -3. **Polling Intervals**: - - Notifications: 60 seconds - - Calendar widget: 5 minutes - - Parole widget: 30 seconds - - Other widgets: On mount only - -4. **Authentication**: All notification and widget APIs require authentication via NextAuth session. - -5. **Error Handling**: All components implement error states with retry mechanisms. - ---- - -## 🔍 Debugging - -- Use `/api/debug/notifications` to test notification system -- Check browser console for detailed logs (all components log extensively) -- Check Redis cache keys: `notifications:count:{userId}`, `notifications:list:{userId}:{page}:{limit}` - ---- - -*Last Updated: Generated from codebase analysis* diff --git a/NOTIFICATION_DEBUG_NEXT_STEPS.md b/NOTIFICATION_DEBUG_NEXT_STEPS.md deleted file mode 100644 index 74e119b3..00000000 --- a/NOTIFICATION_DEBUG_NEXT_STEPS.md +++ /dev/null @@ -1,200 +0,0 @@ -# Notification Mark-All-As-Read Debug - Next Steps - -**Date**: 2026-01-01 -**Status**: Enhanced logging added, awaiting test results - ---- - -## 🔍 Current Situation - -**Issue**: Mark all as read fails, but no detailed error logs are visible - -**Observation from Logs**: -``` -[NOTIFICATION_API] Mark all as read - Failed { userId: '...', duration: '209ms' } -``` - -**Missing Logs**: -- No `[LEANTIME_ADAPTER] markAllAsRead` logs -- No `[NOTIFICATION_SERVICE] markAllAsRead` detailed logs - -**Possible Causes**: -1. Server not restarted with new code -2. Adapter method not being called -3. Error happening before adapter is reached - ---- - -## ✅ Fixes Applied - -### 1. Enhanced Service Layer Logging - -**File**: `lib/services/notifications/notification-service.ts` - -**Added Logging**: -- Logs when `markAllAsRead` is called -- Logs available adapters -- Logs each adapter being processed -- Logs configuration status for each adapter -- Logs when calling adapter's `markAllAsRead` -- Logs result from each adapter -- Logs overall success/failure -- Logs cache invalidation status - -### 2. Enhanced Adapter Layer Logging - -**File**: `lib/services/notifications/leantime-adapter.ts` - -**Added Logging**: -- User email and Leantime user ID -- Request body and API URL -- Response status and body -- Parsed response with error details -- Success/failure status - ---- - -## 🚀 Next Steps - -### Step 1: Restart Server - -**CRITICAL**: The server must be restarted for the new logging to take effect. - -```bash -# Stop the server -sudo npm stop -# Or if using PM2/systemd, restart appropriately - -# Start the server -sudo npm start -``` - -### Step 2: Test Mark All As Read - -1. Open the notification dropdown -2. Click "Mark all as read" -3. Immediately check the server logs - -### Step 3: Check Logs - -**Expected Log Flow** (if working correctly): -``` -[NOTIFICATION_API] Mark all as read endpoint called -[NOTIFICATION_API] Mark all as read - Processing { userId: '...', timestamp: '...' } -[NOTIFICATION_SERVICE] markAllAsRead called for user ... -[NOTIFICATION_SERVICE] Available adapters: leantime -[NOTIFICATION_SERVICE] Processing adapter: leantime -[NOTIFICATION_SERVICE] Adapter leantime is configured: true -[NOTIFICATION_SERVICE] Calling markAllAsRead on adapter leantime -[LEANTIME_ADAPTER] markAllAsRead called for ... -[LEANTIME_ADAPTER] markAllAsRead - User email: ... -[LEANTIME_ADAPTER] markAllAsRead - Leantime user ID: ... -[LEANTIME_ADAPTER] markAllAsRead - Request body: {...} -[LEANTIME_ADAPTER] markAllAsRead - API URL: ... -[LEANTIME_ADAPTER] markAllAsRead - Response status: XXX -[LEANTIME_ADAPTER] markAllAsRead - Response body: {...} -[LEANTIME_ADAPTER] markAllAsRead - Success: true/false -[NOTIFICATION_SERVICE] Adapter leantime markAllAsRead result: true/false -[NOTIFICATION_SERVICE] markAllAsRead overall success: true/false -``` - -**If Still Failing**, the logs will show: -- Which adapter is being processed -- Whether it's configured -- Whether the adapter method is called -- What error the Leantime API returns -- Where exactly it's failing - ---- - -## 🔍 What to Look For - -### If No Adapter Logs Appear - -**Possible Issues**: -1. Server not restarted → **Solution**: Restart server -2. Adapter not configured → Check `isConfigured()` result -3. Error in service layer → Check service layer logs - -### If Adapter Logs Appear But Fail - -**Check These**: -1. **User Email**: Should show email address -2. **Leantime User ID**: Should show numeric ID (e.g., `2`) -3. **Request Body**: Should show valid JSON-RPC request -4. **Response Status**: - - `200` = Success (but check result) - - `400` = Bad request (check error body) - - `401` = Authentication issue - - `500` = Server error -5. **Response Body**: Will show the actual error from Leantime - -### Common Leantime API Errors - -1. **Method Not Found**: - - Error: `"Method not found"` - - Fix: Verify method name is correct - -2. **Invalid Parameters**: - - Error: `"Invalid params"` - - Fix: Check parameter format - -3. **Authentication Failed**: - - Error: `"Unauthorized"` or `401` - - Fix: Check API token - -4. **User Not Found**: - - Error: `"User not found"` - - Fix: Verify Leantime user ID mapping - ---- - -## 📊 Expected Log Output Examples - -### Success Case -``` -[LEANTIME_ADAPTER] markAllAsRead - Response status: 200 -[LEANTIME_ADAPTER] markAllAsRead - Response body: {"jsonrpc":"2.0","result":true,"id":1} -[LEANTIME_ADAPTER] markAllAsRead - Parsed response: { hasResult: true, result: true, hasError: false } -[LEANTIME_ADAPTER] markAllAsRead - Success: true -``` - -### Failure Case - Method Not Found -``` -[LEANTIME_ADAPTER] markAllAsRead - Response status: 200 -[LEANTIME_ADAPTER] markAllAsRead - Response body: {"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":1} -[LEANTIME_ADAPTER] markAllAsRead - Parsed response: { hasResult: false, hasError: true, error: {...} } -[LEANTIME_ADAPTER] markAllAsRead - API Error: { code: -32601, message: "Method not found" } -[LEANTIME_ADAPTER] markAllAsRead - Success: false -``` - -### Failure Case - Invalid Params -``` -[LEANTIME_ADAPTER] markAllAsRead - Response status: 200 -[LEANTIME_ADAPTER] markAllAsRead - Response body: {"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params"},"id":1} -[LEANTIME_ADAPTER] markAllAsRead - API Error: { code: -32602, message: "Invalid params" } -``` - ---- - -## 🎯 Action Items - -1. ✅ **Restart Server** (CRITICAL) -2. ⏳ **Test Mark All As Read** -3. ⏳ **Share Complete Logs** (from endpoint call to failure) -4. ⏳ **Analyze Error Details** (once logs are available) - ---- - -## 📝 Summary - -**Status**: Enhanced logging ready, awaiting server restart and test - -**Next**: After restart, test and share logs to identify exact failure point - -**Confidence**: 🟢 **HIGH** - Enhanced logging will reveal the root cause - ---- - -**Generated**: 2026-01-01 - diff --git a/NOTIFICATION_FIXES_IMPLEMENTED.md b/NOTIFICATION_FIXES_IMPLEMENTED.md deleted file mode 100644 index eb156551..00000000 --- a/NOTIFICATION_FIXES_IMPLEMENTED.md +++ /dev/null @@ -1,239 +0,0 @@ -# Notification System Fixes - Implementation Summary - -**Date**: 2026-01-06 -**Status**: ✅ All fixes implemented - ---- - -## ✅ **Fix #1: Redis Caching for Leantime User ID** - -### **Problem**: -- `getLeantimeUserId()` fetched ALL users from Leantime API every time -- No caching, causing slow performance and inconsistent results -- Race conditions between different calls - -### **Solution**: -- Added Redis caching with 1-hour TTL -- Cache key: `leantime:userid:${email.toLowerCase()}` -- Checks cache first before making API call -- Caches result after successful fetch - -### **Implementation**: -- **File**: `lib/services/notifications/leantime-adapter.ts` -- **Method**: `getLeantimeUserId()` -- **Cache TTL**: 3600 seconds (1 hour) -- **Static helper**: `invalidateUserIdCache()` for manual cache clearing - -### **Benefits**: -- ✅ Faster performance (no API call if cached) -- ✅ More reliable (consistent results) -- ✅ Reduced API load on Leantime -- ✅ Better error recovery (can use cached value if API fails) - ---- - -## ✅ **Fix #2: Retry Logic with Exponential Backoff** - -### **Problem**: -- `getLeantimeUserId()` failed immediately on API errors -- No retry mechanism for transient failures -- Network errors caused permanent failures - -### **Solution**: -- Added retry logic with up to 3 retries -- Exponential backoff: 1s, 2s, 4s (max 5s) -- Retries on: - - Server errors (5xx) - - Rate limiting (429) - - Network errors - - Certain JSON-RPC errors - -### **Implementation**: -- **File**: `lib/services/notifications/leantime-adapter.ts` -- **Method**: `getLeantimeUserId()` with `fetchWithRetry()` -- **Max Retries**: 3 -- **Backoff**: Exponential (1s → 2s → 4s) - -### **Benefits**: -- ✅ Handles transient failures gracefully -- ✅ Better resilience to network issues -- ✅ Improved success rate for user ID lookup - ---- - -## ✅ **Fix #3: Always Invalidate Cache After Marking** - -### **Problem**: -- Cache only invalidated if marking operation succeeded -- If `getLeantimeUserId()` failed, cache stayed stale -- Count remained at old value (65) even after marking attempts - -### **Solution**: -- Always invalidate cache after marking attempt -- Even if operation failed or returned `false` -- Ensures fresh data on next fetch - -### **Implementation**: -- **File**: `lib/services/notifications/notification-service.ts` -- **Methods**: - - `markAsRead()` - Always invalidates cache - - `markAllAsRead()` - Always invalidates cache -- **Logic**: Cache invalidation happens regardless of success/failure - -### **Benefits**: -- ✅ Count always refreshes after marking attempts -- ✅ User sees accurate data even if operation partially failed -- ✅ Better UX (no stale count stuck at 65) - ---- - -## ✅ **Fix #4: Improved Count Accuracy** - -### **Problem**: -- Count only based on first 100 notifications -- If user had >100 notifications, count was inaccurate -- Used cached notifications which might be stale - -### **Solution**: -- Fetch up to 1000 notifications directly from API for counting -- Bypasses cache to get fresh data -- More accurate count for users with many notifications - -### **Implementation**: -- **File**: `lib/services/notifications/leantime-adapter.ts` -- **Method**: `getNotificationCount()` -- **Change**: Fetches directly from API (up to 1000) instead of using cached `getNotifications()` -- **Warning**: Logs if count reaches 1000 (might have more) - -### **Benefits**: -- ✅ More accurate count (up to 1000 notifications) -- ✅ Fresh data (bypasses cache) -- ✅ Better handling of users with many notifications - ---- - -## ✅ **Fix #5: Better Error Handling and Logging** - -### **Problem**: -- Errors were logged but not handled gracefully -- No way to manually clear user ID cache -- Limited error context in logs - -### **Solution**: -- Added static method to invalidate user ID cache -- Improved error messages with more context -- Better logging throughout the flow -- Graceful degradation on errors - -### **Implementation**: -- **File**: `lib/services/notifications/leantime-adapter.ts` -- **Static Method**: `invalidateUserIdCache(email)` -- **Improved Logging**: More detailed error messages -- **Error Recovery**: Continues operation even if caching fails - -### **Benefits**: -- ✅ Better debugging with detailed logs -- ✅ Manual cache clearing for troubleshooting -- ✅ More resilient to partial failures - ---- - -## 📊 **Expected Behavior After Fixes** - -### **Before Fixes**: -1. Mark all as read → `getLeantimeUserId()` fails → Returns `false` -2. Cache NOT invalidated → Count stays 65 ❌ -3. User sees stale count - -### **After Fixes**: -1. Mark all as read → `getLeantimeUserId()` checks cache first ✅ -2. If cached: Uses cached ID immediately ✅ -3. If not cached: Fetches with retry logic ✅ -4. Marks notifications as read ✅ -5. **Always invalidates cache** ✅ -6. Count refresh gets fresh data → Shows 0 ✅ - ---- - -## 🎯 **Key Improvements** - -### **Reliability**: -- ✅ User ID lookup is now cached and retried -- ✅ Cache always invalidated after marking -- ✅ Better error recovery - -### **Performance**: -- ✅ Faster user ID lookup (cached) -- ✅ Reduced API calls to Leantime -- ✅ More efficient cache usage - -### **Accuracy**: -- ✅ Count based on up to 1000 notifications -- ✅ Fresh data from API (bypasses stale cache) -- ✅ Better handling of edge cases - -### **User Experience**: -- ✅ Count updates correctly after marking -- ✅ No more stuck count at 65 -- ✅ Faster response times - ---- - -## 🚀 **Testing Checklist** - -After rebuild (`rm -rf .next && npm run build && npm start`): - -1. ✅ **Test Mark All As Read**: - - Should work even if user ID lookup was previously failing - - Count should update to 0 after marking - - Cache should be invalidated - -2. ✅ **Test Mark Single As Read**: - - Should work reliably - - Count should decrement correctly - - Cache should be invalidated - -3. ✅ **Test Count Accuracy**: - - Should show accurate count (up to 1000) - - Should refresh after marking - - Should use fresh data from API - -4. ✅ **Test User ID Caching**: - - First call should fetch from API - - Subsequent calls should use cache - - Should be faster on subsequent calls - -5. ✅ **Test Retry Logic**: - - Should retry on transient failures - - Should eventually succeed or fail gracefully - - Should log retry attempts - ---- - -## 📝 **Files Modified** - -1. **`lib/services/notifications/leantime-adapter.ts`**: - - Added Redis caching for user ID - - Added retry logic with exponential backoff - - Improved `getNotificationCount()` to fetch directly from API - - Added `invalidateUserIdCache()` static method - - Better error handling and logging - -2. **`lib/services/notifications/notification-service.ts`**: - - Always invalidate cache in `markAsRead()` - - Always invalidate cache in `markAllAsRead()` - - Better error handling and logging - ---- - -## 🔧 **Configuration** - -- **User ID Cache TTL**: 3600 seconds (1 hour) -- **Max Retries**: 3 attempts -- **Retry Backoff**: Exponential (1s, 2s, 4s, max 5s) -- **Count Fetch Limit**: 1000 notifications - ---- - -**Status**: ✅ All fixes implemented and ready for testing - diff --git a/NOTIFICATION_FIXES_IMPLEMENTED_SUMMARY.md b/NOTIFICATION_FIXES_IMPLEMENTED_SUMMARY.md deleted file mode 100644 index 1739fbb2..00000000 --- a/NOTIFICATION_FIXES_IMPLEMENTED_SUMMARY.md +++ /dev/null @@ -1,314 +0,0 @@ -# Notification System Fixes - Implementation Summary - -**Date**: 2026-01-06 -**Status**: ✅ All High-Priority Fixes Implemented - ---- - -## ✅ **Fix #1: Integrated Unified Refresh System** - -### **Changes**: -- **File**: `hooks/use-notifications.ts` -- **Removed**: Custom polling logic (60s interval, debouncing) -- **Added**: `useUnifiedRefresh` hook integration -- **Result**: Uses centralized `RefreshManager` with 30s interval - -### **Benefits**: -- ✅ Consistent refresh intervals across all widgets -- ✅ Reduced code duplication -- ✅ Better coordination with other refresh systems -- ✅ Automatic deduplication built-in - -### **Code Changes**: -```typescript -// Before: Custom polling -pollingIntervalRef.current = setInterval(() => { - debouncedFetchCount(); -}, 60000); - -// After: Unified refresh -const { refresh: refreshCount } = useUnifiedRefresh({ - resource: 'notifications-count', - interval: REFRESH_INTERVALS.NOTIFICATIONS_COUNT, // 30s - enabled: status === 'authenticated', - onRefresh: async () => { - await fetchNotificationCount(false); - }, - priority: 'high', -}); -``` - ---- - -## ✅ **Fix #2: Batch Processing for Mark All As Read** - -### **Changes**: -- **File**: `lib/services/notifications/leantime-adapter.ts` -- **Added**: Batch processing (15 notifications per batch) -- **Added**: Delay between batches (200ms) -- **Added**: Automatic retry for failed notifications -- **Added**: Success rate threshold (80% = success) - -### **Benefits**: -- ✅ Prevents API overload -- ✅ Reduces connection resets -- ✅ Better error recovery -- ✅ More reliable marking - -### **Implementation**: -```typescript -// Process in batches of 15 -const BATCH_SIZE = 15; -const BATCH_DELAY = 200; -const MAX_RETRIES = 2; - -// Process each batch with delay -for (let i = 0; i < notificationIds.length; i += BATCH_SIZE) { - const batch = notificationIds.slice(i, i + BATCH_SIZE); - await Promise.all(batch.map(n => markSingleNotification(n))); - await delay(BATCH_DELAY); // Delay between batches -} - -// Retry failed notifications -if (failedNotifications.length > 0) { - await retryFailedNotifications(); -} -``` - ---- - -## ✅ **Fix #3: Fixed Cache TTL Consistency** - -### **Changes**: -- **File**: `lib/services/notifications/notification-service.ts` -- **Changed**: List cache TTL: 5 minutes → 30 seconds -- **Aligned**: All cache TTLs to 30 seconds -- **File**: `app/api/notifications/route.ts` & `count/route.ts` -- **Changed**: Client cache: `max-age=30/10` → `max-age=0, must-revalidate` - -### **Benefits**: -- ✅ Count and list always in sync -- ✅ Consistent behavior -- ✅ Predictable cache expiration -- ✅ No stale data inconsistencies - -### **Before/After**: -```typescript -// Before -COUNT_CACHE_TTL = 30; // 30 seconds -LIST_CACHE_TTL = 300; // 5 minutes ❌ - -// After -COUNT_CACHE_TTL = 30; // 30 seconds ✅ -LIST_CACHE_TTL = 30; // 30 seconds ✅ -``` - ---- - -## ✅ **Fix #4: Added Progress Feedback** - -### **Changes**: -- **File**: `hooks/use-notifications.ts` -- **Added**: `markingProgress` state: `{ current: number; total: number }` -- **File**: `components/notification-badge.tsx` -- **Added**: Progress bar UI during mark all as read -- **Added**: Progress text: "Marking X of Y..." - -### **Benefits**: -- ✅ User knows operation is in progress -- ✅ Better UX (no silent waiting) -- ✅ Prevents multiple clicks -- ✅ Visual feedback - -### **UI Changes**: -```tsx -{markingProgress && ( -
-
- Marking {markingProgress.current} of {markingProgress.total}... -
-)} -``` - ---- - -## ✅ **Fix #5: Improved Optimistic Updates** - -### **Changes**: -- **File**: `hooks/use-notifications.ts` -- **Added**: Polling mechanism to verify count updates -- **Changed**: Better timing for count refresh -- **Added**: Poll until count matches expected value - -### **Benefits**: -- ✅ More accurate UI updates -- ✅ Less confusing count jumps -- ✅ Better error recovery -- ✅ Verifies server state matches UI - -### **Implementation**: -```typescript -// Poll until count matches expected value -let pollCount = 0; -const maxPolls = 5; -const pollInterval = 500; - -const pollForCount = async () => { - if (pollCount >= maxPolls) return; - pollCount++; - await fetchNotificationCount(true); - if (pollCount < maxPolls) { - setTimeout(pollForCount, pollInterval); - } -}; -``` - ---- - -## ✅ **Fix #6: Added Request Deduplication** - -### **Changes**: -- **File**: `hooks/use-notifications.ts` -- **Added**: `requestDeduplicator` for all fetch calls -- **Result**: Prevents duplicate API calls within 2-second window - -### **Benefits**: -- ✅ Fewer API calls -- ✅ Better performance -- ✅ Reduced server load -- ✅ Prevents race conditions - -### **Implementation**: -```typescript -// Before: Direct fetch -const response = await fetch(url); - -// After: Deduplicated fetch -const data = await requestDeduplicator.execute( - `notifications-count-${userId}`, - async () => { - const response = await fetch(url); - return response.json(); - }, - 2000 // 2 second deduplication window -); -``` - ---- - -## ✅ **Fix #7: Cached User Email** - -### **Changes**: -- **File**: `lib/services/notifications/leantime-adapter.ts` -- **Added**: Redis cache for user email (30-minute TTL) -- **Result**: Reduces session lookups - -### **Benefits**: -- ✅ Better performance -- ✅ Fewer session calls -- ✅ More consistent -- ✅ Reduced overhead - ---- - -## 📊 **Performance Improvements** - -### **Before**: -- Polling: Every 60 seconds -- Cache TTL: Inconsistent (30s / 5min) -- Mark all: All parallel (can timeout) -- No deduplication -- No progress feedback - -### **After**: -- Refresh: Every 30 seconds (unified) -- Cache TTL: Consistent (30s / 30s) -- Mark all: Batched (15 at a time, 200ms delay) -- Request deduplication: 2-second window -- Progress feedback: Real-time UI updates - -### **Expected Results**: -- **50-70% reduction** in API calls -- **30-40% faster** response times -- **80-90% success rate** for mark all (vs 60-70% before) -- **Better UX** with progress indicators - ---- - -## 🎯 **Files Modified** - -1. ✅ `hooks/use-notifications.ts` - - Integrated unified refresh - - Added request deduplication - - Added progress tracking - - Improved optimistic updates - -2. ✅ `lib/services/notifications/leantime-adapter.ts` - - Batch processing for mark all - - Retry logic with exponential backoff - - User email caching - -3. ✅ `lib/services/notifications/notification-service.ts` - - Fixed cache TTL consistency (30s for all) - -4. ✅ `app/api/notifications/route.ts` - - Updated client cache headers - -5. ✅ `app/api/notifications/count/route.ts` - - Updated client cache headers - -6. ✅ `components/notification-badge.tsx` - - Added progress UI - - Better loading states - ---- - -## 🚀 **Testing Checklist** - -After rebuild (`rm -rf .next && npm run build && npm start`): - -1. ✅ **Unified Refresh**: - - Count should refresh every 30 seconds - - Should use centralized refresh manager - - No duplicate polling - -2. ✅ **Batch Processing**: - - Mark all as read should process in batches - - Should show progress (if implemented) - - Should be more reliable (80-90% success) - -3. ✅ **Cache Consistency**: - - Count and list should always be in sync - - Cache should expire after 30 seconds - - No stale data - -4. ✅ **Progress Feedback**: - - Should show progress bar during mark all - - Should display "Marking X of Y..." - - Should prevent multiple clicks - -5. ✅ **Request Deduplication**: - - Multiple rapid calls should be deduplicated - - Should see fewer API calls in logs - - Better performance - ---- - -## 📝 **Next Steps (Optional)** - -### **Medium Priority** (Future): -1. Real-time progress updates (WebSocket/SSE) -2. Connection pooling for API calls -3. Better error messages for users -4. Cancel operation button - -### **Low Priority** (Nice to Have): -1. WebSocket for real-time notifications -2. Push notifications -3. Notification grouping -4. Filtering and sorting - ---- - -**Status**: ✅ All high-priority fixes implemented and ready for testing - diff --git a/NOTIFICATION_FLOW_ANALYSIS.md b/NOTIFICATION_FLOW_ANALYSIS.md deleted file mode 100644 index dc9f22e2..00000000 --- a/NOTIFICATION_FLOW_ANALYSIS.md +++ /dev/null @@ -1,526 +0,0 @@ -# Complete Notification Flow Analysis - -**Date**: 2026-01-06 -**Purpose**: Trace the entire notification system flow to identify issues and improvements - ---- - -## 🔍 **FLOW 1: Initial Page Load & Count Display** - -### Step-by-Step Flow: - -1. **Component Mount** (`notification-badge.tsx`) - - `useNotifications()` hook initializes - - `useEffect` triggers when `status === 'authenticated'` - - Calls `fetchNotificationCount(true)` (force refresh) - - Calls `fetchNotifications()` - - Starts polling every 60 seconds - -2. **Count Fetch** (`use-notifications.ts` → `/api/notifications/count`) - - Hook calls `/api/notifications/count?_t=${Date.now()}` (cache-busting) - - API route authenticates user - - Calls `NotificationService.getNotificationCount(userId)` - -3. **Service Layer** (`notification-service.ts`) - - **Checks Redis cache first** (`notifications:count:${userId}`) - - If cached: Returns cached data immediately - - If not cached: Fetches from adapters - -4. **Adapter Layer** (`leantime-adapter.ts`) - - `getNotificationCount()` calls `getNotifications(userId, 1, 100)` - - **⚠️ ISSUE**: Only fetches first 100 notifications for counting - - Filters unread: `notifications.filter(n => !n.isRead).length` - - Returns count object - -5. **Cache Storage** - - Service stores count in Redis with 30-second TTL - - Returns to API route - - API returns to hook - - Hook updates React state: `setNotificationCount(data)` - -6. **UI Update** - - Badge displays `notificationCount.unread` - - Shows "65" if 65 unread notifications - ---- - -## 🔍 **FLOW 2: Mark Single Notification as Read** - -### Step-by-Step Flow: - -1. **User Action** (`notification-badge.tsx`) - - User clicks "Mark as read" button - - Calls `handleMarkAsRead(notificationId)` - - Calls `markAsRead(notificationId)` from hook - -2. **Hook Action** (`use-notifications.ts`) - - Makes POST to `/api/notifications/${notificationId}/read` - - **Optimistic UI Update**: - - Updates notification in state: `isRead: true` - - Decrements count: `unread: Math.max(0, prev.unread - 1)` - - Waits 100ms, then calls `fetchNotificationCount(true)` - -3. **API Route** (`app/api/notifications/[id]/read/route.ts`) - - Authenticates user - - Extracts notification ID: `leantime-2732` → splits to get source and ID - - Calls `NotificationService.markAsRead(userId, notificationId)` - -4. **Service Layer** (`notification-service.ts`) - - Extracts source: `leantime` from ID - - Gets adapter: `this.adapters.get('leantime')` - - Calls `adapter.markAsRead(userId, notificationId)` - -5. **Adapter Layer** (`leantime-adapter.ts`) - - **Gets user email from session**: `getUserEmail()` - - **Gets Leantime user ID**: `getLeantimeUserId(email)` - - **⚠️ CRITICAL ISSUE**: If `getLeantimeUserId()` fails → returns `false` - - If successful: Calls Leantime API `markNotificationRead` - - Returns success/failure - -6. **Cache Invalidation** (`notification-service.ts`) - - If `markAsRead()` returns `true`: - - Calls `invalidateCache(userId)` - - Deletes count cache: `notifications:count:${userId}` - - Deletes all list caches: `notifications:list:${userId}:*` - - If returns `false`: **Cache NOT invalidated** ❌ - -7. **Count Refresh** (`use-notifications.ts`) - - After 100ms delay, calls `fetchNotificationCount(true)` - - Fetches fresh count from API - - **⚠️ ISSUE**: If cache wasn't invalidated, might get stale count - ---- - -## 🔍 **FLOW 3: Mark All Notifications as Read** - -### Step-by-Step Flow: - -1. **User Action** (`notification-badge.tsx`) - - User clicks "Mark all read" button - - Calls `handleMarkAllAsRead()` - - Calls `markAllAsRead()` from hook - -2. **Hook Action** (`use-notifications.ts`) - - Makes POST to `/api/notifications/read-all` - - **Optimistic UI Update**: - - Sets all notifications: `isRead: true` - - Sets count: `unread: 0` - - Waits 200ms, then calls `fetchNotificationCount(true)` - -3. **API Route** (`app/api/notifications/read-all/route.ts`) - - Authenticates user - - Calls `NotificationService.markAllAsRead(userId)` - -4. **Service Layer** (`notification-service.ts`) - - Loops through all adapters - - For each adapter: - - Checks if configured - - Calls `adapter.markAllAsRead(userId)` - - Collects results: `[true/false, ...]` - - Determines: `success = results.every(r => r)`, `anySuccess = results.some(r => r)` - - **Cache Invalidation**: - - If `anySuccess === true`: Invalidates cache ✅ - - If `anySuccess === false`: **Cache NOT invalidated** ❌ - -5. **Adapter Layer** (`leantime-adapter.ts`) - - **Gets user email**: `getUserEmail()` - - **Gets Leantime user ID**: `getLeantimeUserId(email)` - - **⚠️ CRITICAL ISSUE**: If this fails → returns `false` immediately - - If successful: - - Fetches all notifications directly from API (up to 1000) - - Filters unread: `rawNotifications.filter(n => n.read === 0)` - - Marks each individually using `markNotificationRead` - - Returns success if any were marked - -6. **Cache Invalidation** (`notification-service.ts`) - - Only happens if `anySuccess === true` - - **⚠️ ISSUE**: If `getLeantimeUserId()` fails, `anySuccess = false` - - Cache stays stale → count remains 65 - -7. **Count Refresh** (`use-notifications.ts`) - - After 200ms, calls `fetchNotificationCount(true)` - - **⚠️ ISSUE**: If cache wasn't invalidated, gets stale count from cache - ---- - -## 🔍 **FLOW 4: Fetch Notification List** - -### Step-by-Step Flow: - -1. **User Opens Dropdown** (`notification-badge.tsx`) - - `handleOpenChange(true)` called - - Calls `manualFetch()` which calls `fetchNotifications(1, 10)` - -2. **Hook Action** (`use-notifications.ts`) - - Makes GET to `/api/notifications?page=1&limit=20` - - Updates state: `setNotifications(data.notifications)` - -3. **API Route** (`app/api/notifications/route.ts`) - - Authenticates user - - Calls `NotificationService.getNotifications(userId, page, limit)` - -4. **Service Layer** (`notification-service.ts`) - - **Checks Redis cache first**: `notifications:list:${userId}:${page}:${limit}` - - If cached: Returns cached data immediately - - If not cached: Fetches from adapters - -5. **Adapter Layer** (`leantime-adapter.ts`) - - Gets user email and Leantime user ID - - Calls Leantime API `getAllNotifications` with pagination - - Transforms notifications to our format - - Returns array - -6. **Cache Storage** - - Service stores list in Redis with 5-minute TTL - - Returns to API - - API returns to hook - - Hook updates React state - ---- - -## 🐛 **IDENTIFIED ISSUES** - -### **Issue #1: getLeantimeUserId() Fails Inconsistently** - -**Problem**: -- `getLeantimeUserId()` works in `getNotifications()` and `getNotificationCount()` -- But fails in `markAllAsRead()` and sometimes in `markAsRead()` -- Logs show: `"User not found in Leantime: a.tmiri@clm.foundation"` - -**Root Cause**: -- `getLeantimeUserId()` calls Leantime API `getAll` users endpoint -- Fetches ALL users, then searches for matching email -- **Possible causes**: - 1. **Race condition**: API call happens at different times - 2. **Session timing**: Session might be different between calls - 3. **API rate limiting**: Leantime API might throttle requests - 4. **Caching issue**: No caching of user ID lookup - -**Impact**: -- Mark all as read fails → cache not invalidated → count stays 65 -- Mark single as read might fail → cache not invalidated → count doesn't update - -**Solution**: -- Cache Leantime user ID in Redis with longer TTL -- Add retry logic with exponential backoff -- Add better error handling and logging - ---- - -### **Issue #2: Cache Invalidation Only on Success** - -**Problem**: -- Cache is only invalidated if `markAsRead()` or `markAllAsRead()` returns `true` -- If operation fails (e.g., `getLeantimeUserId()` fails), cache stays stale -- Count remains at old value (65) - -**Root Cause**: -```typescript -if (success) { - await this.invalidateCache(userId); -} -``` - -**Impact**: -- User sees stale count even after attempting to mark as read -- UI shows optimistic update, but server count doesn't match - -**Solution**: -- Always invalidate cache after marking attempt (even on failure) -- Or: Invalidate cache before marking, then refresh after -- Or: Use optimistic updates with eventual consistency - ---- - -### **Issue #3: Count Based on First 100 Notifications** - -**Problem**: -- `getNotificationCount()` only fetches first 100 notifications -- If user has 200 notifications with 66 unread, count shows 66 -- But if 66 unread are beyond first 100, count is wrong - -**Root Cause**: -```typescript -const notifications = await this.getNotifications(userId, 1, 100); -const unreadCount = notifications.filter(n => !n.isRead).length; -``` - -**Impact**: -- Count might be inaccurate if >100 notifications exist -- User might see "66 unread" but only 10 displayed (pagination) - -**Solution**: -- Use dedicated count API if Leantime provides one -- Or: Fetch all notifications for counting (up to reasonable limit) -- Or: Show "66+ unread" if count reaches 100 - ---- - -### **Issue #4: Race Condition Between Cache Invalidation and Count Fetch** - -**Problem**: -- Hook calls `fetchNotificationCount(true)` after 100-200ms delay -- But cache invalidation might not be complete -- Count fetch might still get stale cache - -**Root Cause**: -```typescript -setTimeout(() => { - fetchNotificationCount(true); -}, 200); -``` - -**Impact**: -- Count might not update immediately after marking -- User sees optimistic update, then stale count - -**Solution**: -- Increase delay to 500ms -- Or: Poll count until it matches expected value -- Or: Use WebSocket/SSE for real-time updates - ---- - -### **Issue #5: No Caching of Leantime User ID** - -**Problem**: -- `getLeantimeUserId()` fetches ALL users from Leantime API every time -- No caching, so repeated calls are slow and might fail -- Different calls might get different results (race condition) - -**Root Cause**: -- No Redis cache for user ID mapping -- Each call makes full API request - -**Impact**: -- Slow performance -- Inconsistent results -- API rate limiting issues - -**Solution**: -- Cache user ID in Redis: `leantime:userid:${email}` with 1-hour TTL -- Invalidate cache only when user changes or on explicit refresh - ---- - -### **Issue #6: getNotificationCount Uses Cached getNotifications** - -**Problem**: -- `getNotificationCount()` calls `getNotifications(userId, 1, 100)` -- `getNotifications()` uses cache if available -- Count might be based on stale cached notifications - -**Root Cause**: -```typescript -async getNotificationCount(userId: string): Promise { - const notifications = await this.getNotifications(userId, 1, 100); - // Uses cached data if available -} -``` - -**Impact**: -- Count might be stale even if notifications were marked as read -- Cache TTL mismatch: count cache (30s) vs list cache (5min) - -**Solution**: -- Fetch notifications directly from API for counting (bypass cache) -- Or: Use dedicated count endpoint -- Or: Invalidate list cache when count cache is invalidated - ---- - -### **Issue #7: Optimistic Updates Don't Match Server State** - -**Problem**: -- Hook optimistically updates count: `unread: 0` -- But server count might still be 65 (cache not invalidated) -- After refresh, count jumps back to 65 - -**Root Cause**: -- Optimistic update happens immediately -- Server cache invalidation might fail -- Count refresh gets stale data - -**Impact**: -- Confusing UX: count goes to 0, then back to 65 -- User thinks operation failed when it might have succeeded - -**Solution**: -- Only show optimistic update if we're confident operation will succeed -- Or: Show loading state until server confirms -- Or: Poll until count matches expected value - ---- - -## 🎯 **RECOMMENDED IMPROVEMENTS** - -### **Priority 1: Fix getLeantimeUserId() Reliability** - -1. **Cache User ID Mapping** - ```typescript - // Cache key: leantime:userid:${email} - // TTL: 1 hour - // Invalidate on user update or explicit refresh - ``` - -2. **Add Retry Logic** - ```typescript - // Retry 3 times with exponential backoff - // Log each attempt - // Return cached value if API fails - ``` - -3. **Better Error Handling** - ```typescript - // Log full error details - // Return null only after all retries fail - // Don't fail entire operation on user ID lookup failure - ``` - ---- - -### **Priority 2: Always Invalidate Cache After Marking** - -1. **Invalidate Before Marking** - ```typescript - // Invalidate cache first - // Then mark as read - // Then refresh count - ``` - -2. **Or: Always Invalidate After Attempt** - ```typescript - // Always invalidate cache after marking attempt - // Even if operation failed - // This ensures fresh data on next fetch - ``` - ---- - -### **Priority 3: Fix Count Accuracy** - -1. **Use Dedicated Count API** (if available) - ```typescript - // Check if Leantime has count-only endpoint - // Use that instead of fetching all notifications - ``` - -2. **Or: Fetch All for Counting** - ```typescript - // Fetch up to 1000 notifications for counting - // Or use pagination to count all - ``` - -3. **Or: Show "66+ unread" if limit reached** - ```typescript - // If count === 100, show "100+ unread" - // Indicate there might be more - ``` - ---- - -### **Priority 4: Improve Cache Strategy** - -1. **Unified Cache Invalidation** - ```typescript - // When count cache is invalidated, also invalidate list cache - // When list cache is invalidated, also invalidate count cache - // Keep them in sync - ``` - -2. **Shorter Cache TTLs** - ```typescript - // Count cache: 10 seconds (currently 30s) - // List cache: 1 minute (currently 5min) - // More frequent updates - ``` - -3. **Cache Tags/Versioning** - ```typescript - // Use cache version numbers - // Increment on invalidation - // Check version before using cache - ``` - ---- - -### **Priority 5: Better Error Recovery** - -1. **Graceful Degradation** - ```typescript - // If mark as read fails, still invalidate cache - // Show error message to user - // Allow retry - ``` - -2. **Retry Logic** - ```typescript - // Retry failed operations automatically - // Exponential backoff - // Max 3 retries - ``` - ---- - -## 📊 **FLOW DIAGRAM: Current vs Improved** - -### **Current Flow (Mark All As Read)**: -``` -User clicks → Hook → API → Service → Adapter - ↓ -getLeantimeUserId() → FAILS ❌ - ↓ -Returns false → Service: anySuccess = false - ↓ -Cache NOT invalidated ❌ - ↓ -Count refresh → Gets stale cache → Shows 65 ❌ -``` - -### **Improved Flow (Mark All As Read)**: -``` -User clicks → Hook → API → Service → Adapter - ↓ -getLeantimeUserId() → Check cache first - ↓ -If cached: Use cached ID ✅ -If not cached: Fetch from API → Cache result ✅ - ↓ -Mark all as read → Success ✅ - ↓ -Always invalidate cache (even on partial failure) ✅ - ↓ -Count refresh → Gets fresh data → Shows 0 ✅ -``` - ---- - -## 🚀 **IMPLEMENTATION PRIORITY** - -1. **Fix getLeantimeUserId() caching** (High Priority) - - Add Redis cache for user ID mapping - - Add retry logic - - Better error handling - -2. **Always invalidate cache** (High Priority) - - Invalidate cache even on failure - - Or invalidate before marking - -3. **Fix count accuracy** (Medium Priority) - - Use dedicated count API or fetch all - - Show "66+ unread" if limit reached - -4. **Improve cache strategy** (Medium Priority) - - Unified invalidation - - Shorter TTLs - - Cache versioning - -5. **Better error recovery** (Low Priority) - - Graceful degradation - - Retry logic - - Better UX - ---- - -**Status**: Analysis complete. Ready for implementation. - diff --git a/NOTIFICATION_ISSUES_FIX.md b/NOTIFICATION_ISSUES_FIX.md deleted file mode 100644 index 6713ed09..00000000 --- a/NOTIFICATION_ISSUES_FIX.md +++ /dev/null @@ -1,194 +0,0 @@ -# Notification Issues - Analysis & Fixes - -**Date**: 2026-01-01 -**Issues Reported**: -1. Count shows 66 messages, but only 10 are displayed -2. "Mark all as read" fails -3. Count doesn't update after marking as read - ---- - -## 🔍 Issue Analysis - -### Issue 1: Count vs Display Discrepancy - -**Symptom**: -- Badge shows: **66 unread notifications** -- Dropdown shows: **Only 10 notifications** - -**Root Cause**: -1. **Count Logic**: `getNotificationCount()` calls `getNotifications(userId, 1, 100)` to count - - Gets first 100 notifications from Leantime - - Counts unread: 66 - - This is correct for the first 100 notifications - -2. **Display Logic**: `getNotifications()` is called with `limit: 20` (default) - - But only 10 are shown (possibly due to pagination or filtering) - - This is a display/pagination issue - -**The Problem**: -- If Leantime has more than 100 notifications total, the count will be inaccurate -- The count only reflects the first 100 notifications -- Display shows fewer notifications than the count - -**Solution**: -- ✅ Added warning log when count reaches 100 (may have more) -- ⚠️ Consider using a dedicated count API if Leantime provides one -- ⚠️ Consider fetching all notifications for accurate count (may be slow) - ---- - -### Issue 2: Mark All As Read Fails - -**Symptom**: -``` -[NOTIFICATION_API] Mark all as read - Failed { userId: '...', duration: '197ms' } -``` - -**Root Cause**: -- Leantime API call is failing -- No detailed error logging to see why - -**Solution Applied**: -- ✅ Added comprehensive error logging to `markAllAsRead()`: - - Logs user email and Leantime user ID - - Logs request body and API URL - - Logs response status and body - - Logs parsed response with error details - - Logs exceptions with stack traces - -**Next Steps**: -1. Test mark-all-as-read again -2. Check logs for detailed error information -3. Verify Leantime API method name is correct -4. Check if Leantime API requires different parameters - ---- - -## 🔧 Fixes Applied - -### 1. Enhanced Error Logging in `markAllAsRead` - -**File**: `lib/services/notifications/leantime-adapter.ts` - -**Changes**: -- Added detailed logging at each step -- Logs request details (body, URL) -- Logs response details (status, body, parsed data) -- Logs errors with full context -- Logs success/failure status - -**Expected Log Output**: -``` -[LEANTIME_ADAPTER] markAllAsRead called for ... -[LEANTIME_ADAPTER] markAllAsRead - User email: ... -[LEANTIME_ADAPTER] markAllAsRead - Leantime user ID: ... -[LEANTIME_ADAPTER] markAllAsRead - Request body: {...} -[LEANTIME_ADAPTER] markAllAsRead - API URL: ... -[LEANTIME_ADAPTER] markAllAsRead - Response status: 200 -[LEANTIME_ADAPTER] markAllAsRead - Response body: {...} -[LEANTIME_ADAPTER] markAllAsRead - Parsed response: {...} -[LEANTIME_ADAPTER] markAllAsRead - Success: true/false -``` - ---- - -### 2. Enhanced Count Logging - -**File**: `lib/services/notifications/leantime-adapter.ts` - -**Changes**: -- Added warning when count reaches 100 (may have more notifications) -- Added read count to logging -- Added note about potential inaccuracy - ---- - -## 🎯 Next Steps - -### Immediate Testing - -1. **Test Mark All As Read** - - Click "Mark all as read" - - Check logs for detailed error information - - Look for `[LEANTIME_ADAPTER] markAllAsRead` entries - -2. **Verify Count Accuracy** - - Check if Leantime has more than 100 notifications - - Verify count matches actual unread notifications - - Check if count updates after marking as read - -### Potential Issues to Check - -1. **Leantime API Method Name** - - Current: `leantime.rpc.Notifications.Notifications.markAllNotificationsAsRead` - - Verify this is the correct method name in Leantime API - -2. **Leantime API Parameters** - - Current: `{ userId: leantimeUserId }` - - May need additional parameters - -3. **Leantime API Response Format** - - Check if response format matches expected format - - May need to handle different response structures - ---- - -## 📊 Expected Behavior After Fixes - -### Mark All As Read - -**Success Case**: -``` -[NOTIFICATION_API] Mark all as read endpoint called -[NOTIFICATION_API] Mark all as read - Processing { userId: '...', timestamp: '...' } -[LEANTIME_ADAPTER] markAllAsRead called for ... -[LEANTIME_ADAPTER] markAllAsRead - Success: true -[NOTIFICATION_API] Mark all as read - Success { userId: '...', duration: 'Xms' } -[NOTIFICATION_SERVICE] Invalidated notification caches for user ... -``` - -**Failure Case** (with detailed error): -``` -[NOTIFICATION_API] Mark all as read endpoint called -[LEANTIME_ADAPTER] markAllAsRead called for ... -[LEANTIME_ADAPTER] markAllAsRead - Response status: 400 -[LEANTIME_ADAPTER] markAllAsRead - Response body: {"error": {...}} -[LEANTIME_ADAPTER] markAllAsRead - API Error: {...} -[NOTIFICATION_API] Mark all as read - Failed { userId: '...', duration: 'Xms' } -``` - ---- - -## 🔍 Debugging Checklist - -When testing, check logs for: - -- [ ] `[LEANTIME_ADAPTER] markAllAsRead - User email:` (should show email) -- [ ] `[LEANTIME_ADAPTER] markAllAsRead - Leantime user ID:` (should show ID) -- [ ] `[LEANTIME_ADAPTER] markAllAsRead - Request body:` (should show JSON-RPC request) -- [ ] `[LEANTIME_ADAPTER] markAllAsRead - Response status:` (should be 200 for success) -- [ ] `[LEANTIME_ADAPTER] markAllAsRead - Response body:` (should show API response) -- [ ] `[LEANTIME_ADAPTER] markAllAsRead - Parsed response:` (should show result/error) -- [ ] `[LEANTIME_ADAPTER] markAllAsRead - Success:` (should be true/false) - ---- - -## 📝 Summary - -**Fixes Applied**: -1. ✅ Enhanced error logging in `markAllAsRead` -2. ✅ Enhanced count logging with warnings - -**Next Actions**: -1. Test mark-all-as-read functionality -2. Review detailed error logs -3. Fix Leantime API call based on error details -4. Verify count accuracy - -**Status**: ⏳ **AWAITING TESTING** - Enhanced logging will reveal the root cause - ---- - -**Generated**: 2026-01-01 - diff --git a/NOTIFICATION_ISSUE_ANALYSIS.md b/NOTIFICATION_ISSUE_ANALYSIS.md deleted file mode 100644 index 8e1800cb..00000000 --- a/NOTIFICATION_ISSUE_ANALYSIS.md +++ /dev/null @@ -1,202 +0,0 @@ -# Notification Issue Analysis - Mark All Read Behavior - -**Date**: 2026-01-06 -**Issue**: Mark all read works initially, then connection issues occur - ---- - -## 🔍 **What's Happening** - -### **Initial Success**: -1. ✅ Dashboard shows 60 messages (count is working) -2. ✅ User clicks "Mark all read" -3. ✅ **First step works** - Marking operation starts successfully - -### **Then Connection Issues**: -``` -failed to get redirect response [TypeError: fetch failed] { - [cause]: [Error: read ECONNRESET] { - errno: -104, - code: 'ECONNRESET', - syscall: 'read' - } -} -Redis reconnect attempt 1, retrying in 100ms -Reconnecting to Redis.. -``` - ---- - -## 📊 **Analysis** - -### **What the Logs Show**: - -1. **IMAP Pool Activity**: - ``` - [IMAP POOL] Size: 1, Active: 1, Connecting: 0, Max: 20 - [IMAP POOL] Size: 0, Active: 0, Connecting: 0, Max: 20 - ``` - - IMAP connections are being used and released - - This is normal behavior - -2. **Connection Reset Error**: - - `ECONNRESET` - Connection was reset by peer - - Happens during a fetch request (likely to Leantime API) - - This is a **network/connection issue**, not a code issue - -3. **Redis Reconnection**: - - Redis is trying to reconnect (expected behavior) - - Our retry logic is working - ---- - -## 🎯 **Root Cause** - -### **Scenario**: -1. User clicks "Mark all read" -2. System starts marking notifications (works initially) -3. During the process, a network connection to Leantime API is reset -4. This could happen because: - - **Network instability** between your server and Leantime - - **Leantime API timeout** (if marking many notifications takes too long) - - **Connection pool exhaustion** (too many concurrent requests) - - **Server-side rate limiting** (Leantime might be throttling requests) - -### **Why It Works Initially Then Fails**: -- **First few notifications**: Marked successfully ✅ -- **After some time**: Connection resets ❌ -- **Result**: Partial success (some marked, some not) - ---- - -## 🔧 **What Our Fixes Handle** - -### **✅ What's Working**: -1. **User ID Caching**: Should prevent the "user not found" error -2. **Retry Logic**: Will retry failed requests automatically -3. **Cache Invalidation**: Always happens, so count will refresh -4. **Count Accuracy**: Fetches up to 1000 notifications - -### **⚠️ What's Not Handled**: -1. **Long-running operations**: Marking 60 notifications individually can take time -2. **Connection timeouts**: If Leantime API is slow or times out -3. **Rate limiting**: If Leantime throttles too many requests -4. **Partial failures**: Some notifications marked, some not - ---- - -## 💡 **What's Likely Happening** - -### **Flow**: -``` -1. User clicks "Mark all read" - ↓ -2. System fetches 60 unread notifications ✅ - ↓ -3. Starts marking each one individually - ↓ -4. First 10-20 succeed ✅ - ↓ -5. Connection resets (ECONNRESET) ❌ - ↓ -6. Remaining notifications fail to mark - ↓ -7. Cache is invalidated (our fix) ✅ - ↓ -8. Count refresh shows remaining unread (e.g., 40 instead of 0) -``` - -### **Why Count Might Not Be 0**: -- Some notifications were marked (e.g., 20 out of 60) -- Connection reset prevented marking the rest -- Cache was invalidated (good!) -- Count refresh shows remaining unread (40 unread) - ---- - -## 🎯 **Expected Behavior** - -### **With Our Fixes**: -1. ✅ User ID lookup is cached (faster, more reliable) -2. ✅ Retry logic handles transient failures -3. ✅ Cache always invalidated (count will refresh) -4. ✅ Count shows accurate number (up to 1000) - -### **What You Should See**: -- **First attempt**: Some notifications marked, count decreases (e.g., 60 → 40) -- **Second attempt**: More notifications marked, count decreases further (e.g., 40 → 20) -- **Eventually**: All marked, count reaches 0 - -### **If Connection Issues Persist**: -- Count will show remaining unread -- User can retry "Mark all read" -- Each retry will mark more notifications -- Eventually all will be marked - ---- - -## 🔍 **Diagnostic Questions** - -1. **How many notifications are marked?** - - Check if count decreases (e.g., 60 → 40 → 20 → 0) - - If it decreases, marking is working but incomplete - -2. **Does retry help?** - - Click "Mark all read" again - - If count decreases further, retry logic is working - -3. **Is it always the same number?** - - If count always stops at same number (e.g., always 40), might be specific notifications failing - - If count varies, it's likely connection issues - -4. **Network stability?** - - Check if connection to Leantime API is stable - - Monitor for timeouts or rate limiting - ---- - -## 📝 **Recommendations** - -### **Immediate**: -1. **Retry the operation**: Click "Mark all read" again - - Should mark more notifications - - Count should decrease further - -2. **Check logs for specific errors**: - - Look for which notification IDs are failing - - Check if it's always the same ones - -3. **Monitor network**: - - Check connection stability to Leantime - - Look for timeout patterns - -### **Future Improvements** (if needed): -1. **Batch marking**: Mark notifications in smaller batches (e.g., 10 at a time) -2. **Progress indicator**: Show "Marking X of Y..." to user -3. **Resume on failure**: Track which notifications were marked, resume from where it failed -4. **Connection pooling**: Better management of concurrent requests - ---- - -## ✅ **Summary** - -### **What's Working**: -- ✅ Initial marking starts successfully -- ✅ User ID caching prevents lookup failures -- ✅ Cache invalidation ensures count refreshes -- ✅ Retry logic handles transient failures - -### **What's Failing**: -- ⚠️ Connection resets during long operations -- ⚠️ Partial marking (some succeed, some fail) -- ⚠️ Network instability between server and Leantime - -### **Solution**: -- **Retry the operation**: Click "Mark all read" multiple times -- Each retry should mark more notifications -- Eventually all will be marked - ---- - -**Status**: This is expected behavior with network issues. The fixes ensure the system recovers and continues working. - diff --git a/README-MINIO-TROUBLESHOOTING.md b/README-MINIO-TROUBLESHOOTING.md deleted file mode 100644 index a21ad38b..00000000 --- a/README-MINIO-TROUBLESHOOTING.md +++ /dev/null @@ -1,115 +0,0 @@ -# Minio Troubleshooting Guide - -This document outlines the fixes implemented for the mission file upload issues with Minio. - -## Problem Description - -Mission uploads (logo and attachments) were not working correctly: -- Files weren't appearing in Minio despite upload attempts -- Mission logos weren't displaying even though they were uploaded -- Participation field showed "Non spécifié" despite values in the database -- SDG/ODD icons weren't displaying correctly - -## Implemented Fixes - -### 1. Added URL Generation Function - -Added a `getPublicUrl` function in `lib/s3.ts` that properly constructs URLs for files stored in Minio: -```typescript -export function getPublicUrl(filePath: string): string { - if (!filePath) return ''; - if (filePath.startsWith('http')) return filePath; // Already a full URL - - // Remove leading slash if present - const cleanPath = filePath.startsWith('/') ? filePath.substring(1) : filePath; - - // Construct the full URL - const endpoint = S3_CONFIG.endpoint?.replace(/\/$/, ''); // Remove trailing slash if present - const bucket = S3_CONFIG.bucket; - - // Return original path if no endpoint is configured - if (!endpoint) return cleanPath; - - // Construct and return the full URL - return `${endpoint}/${bucket}/${cleanPath}`; -} -``` - -### 2. Updated Mission Display Page - -Modified `app/missions/page.tsx` to use the `getPublicUrl` function when displaying mission logos: -```tsx -{mission.logo ? ( - {mission.name} { - // Error handling... - }} - /> -) : null} -``` - -### 3. Enhanced Upload API - -Updated `/app/api/missions/upload/route.ts` to: -- Include additional logging -- Generate and return proper public URLs -- Improve error handling - -### 4. Enhanced Mission Detail API - -Modified `/app/api/missions/[missionId]/route.ts` to include public URLs in the response: -```typescript -const missionWithUrls = { - ...mission, - logoUrl: mission.logo ? getPublicUrl(mission.logo) : null, - attachments: mission.attachments.map((attachment) => ({ - ...attachment, - publicUrl: getPublicUrl(attachment.filePath) - })) -}; -``` - -### 5. Added Testing Tools - -1. Browser Console Utilities: - - `window.testMinioConnection()` - Test Minio connectivity - - `window.getMinioUrl(path)` - Generate a public URL for debugging - -2. Server-side Test Script: - - Created `scripts/test-minio-upload.js` to test uploads from the command line - - Tests uploading, downloading, and URL generation - -## How to Test - -1. **Using the browser console:** - ```javascript - // Test connection and list files - window.testMinioConnection() - - // Generate URL for a specific path - window.getMinioUrl('user-123/missions/456/logo.jpg') - ``` - -2. **Using the server-side script:** - ```bash - node scripts/test-minio-upload.js - ``` - -## Required Environment Variables - -Make sure these are properly set in your environment: -- `MINIO_S3_UPLOAD_BUCKET_URL` - The Minio endpoint URL -- `MINIO_AWS_REGION` - The AWS region (often 'us-east-1' for Minio) -- `MINIO_AWS_S3_UPLOAD_BUCKET_NAME` - The bucket name -- `MINIO_ACCESS_KEY` - Access key for Minio -- `MINIO_SECRET_KEY` - Secret key for Minio - -## Additional Notes - -1. The same Minio bucket is used for both Pages and Missions. -2. Pages functionality is working properly, suggesting the Minio configuration itself is correct. -3. Make sure that the bucket has proper permissions for public read access. -4. The URL paths for SDG/ODD icons were corrected to use `/F SDG Icons 2019 WEB/F-WEB-Goal-XX.png` \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 550c914d..00000000 --- a/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# Neah Email Application - -A modern email client built with Next.js, featuring email composition, viewing, and management capabilities. - -## Email Processing Workflow - -The application handles email processing through a centralized workflow: - -1. **Email Fetching**: Emails are fetched through the `/api/courrier` endpoints using user credentials stored in the database. - -2. **Email Parsing**: Raw email content is parsed using: - - Server-side: `parseEmail` function from `lib/server/email-parser.ts` (which uses `simpleParser` from the `mailparser` library) - - API route: `/api/parse-email` provides a REST interface to the parser - -3. **HTML Sanitization**: Email HTML content is sanitized and processed using: - - `sanitizeHtml` function in `lib/utils/email-utils.ts` (centralized implementation) - - DOMPurify with specific configuration to handle email content safely - -4. **Email Display**: Sanitized content is rendered in the UI with proper styling and security measures - -5. **Email Composition**: The `ComposeEmail` component handles email creation, replying, and forwarding - - Email is sent through the `/api/courrier/send` endpoint - -## Key Features - -- **Email Fetching and Management**: Connect to IMAP servers and manage email fetching and caching logic -- **Email Composition**: Rich text editor with reply and forwarding capabilities -- **Email Display**: Secure rendering of HTML emails -- **Attachment Handling**: View and download attachments - -## Project Structure - -The project follows a modular structure: - -- `/app` - Next.js App Router structure with routes and API endpoints -- `/components` - React components organized by domain -- `/lib` - Core library code: - - `/server` - Server-only code like email parsing - - `/services` - Domain-specific services, including email service - - `/reducers` - State management logic - - `/utils` - Utility functions including the centralized email formatter - -## Technologies - -- Next.js 14+ with App Router -- React Server Components -- TailwindCSS for styling -- Mailparser for email parsing -- ImapFlow for email fetching -- DOMPurify for HTML sanitization -- Redis for caching - -## State Management - -Email state is managed through React context and reducers, with server data fetched through React Server Components or client-side API calls as needed. - -# Email Formatting - -## Centralized Email Formatter - -All email formatting is now handled by a centralized formatter in `lib/utils/email-utils.ts`. This ensures consistent handling of: - -- Reply and forward formatting -- HTML sanitization -- RTL/LTR text direction -- MIME encoding and decoding for email composition - -Key functions include: -- `formatForwardedEmail`: Format emails for forwarding -- `formatReplyEmail`: Format emails for replying -- `sanitizeHtml`: Safely sanitize HTML email content -- `formatEmailForReplyOrForward`: Compatibility function for both -- `decodeComposeContent`: Parse MIME content for email composition -- `encodeComposeContent`: Create MIME-formatted content for sending emails - -This centralized approach prevents formatting inconsistencies and direction problems when dealing with emails in different languages. - -## Deprecated Functions - -Several functions have been deprecated and removed in favor of centralized implementations: - -- Check the `DEPRECATED_FUNCTIONS.md` file for a complete list of deprecated functions and their replacements. - -## User Management API - -The application provides endpoints for managing users in multiple systems: - -- **Create User**: - - Endpoint: `POST /api/users` - - Creates users in Keycloak, Leantime, and Dolibarr (if they have "mediation" or "expression" roles) - -- **Update User**: - - Endpoint: `PUT /api/users/[userId]` - - Updates user details in Keycloak - -- **Delete User**: - - Endpoint: `DELETE /api/users?id=[userId]&email=[userEmail]` - - Deletes users from Keycloak, Leantime, and Dolibarr systems - - **Important**: Always include both `id` and `email` parameters for complete deletion across all systems - - The legacy endpoint `DELETE /api/users/[userId]` forwards to the above endpoint - -- **Manage Roles**: - - Endpoint: `PUT /api/users/[userId]/roles` - - Updates user roles in Keycloak - -- **Reset Password**: - - Endpoint: `PUT /api/users/[userId]/password` - - Resets user password in Keycloak \ No newline at end of file diff --git a/SEPARATED_AUTHENTICATION_FLOWS_EXPLANATION.md b/SEPARATED_AUTHENTICATION_FLOWS_EXPLANATION.md deleted file mode 100644 index 5e9111bc..00000000 --- a/SEPARATED_AUTHENTICATION_FLOWS_EXPLANATION.md +++ /dev/null @@ -1,366 +0,0 @@ -# Why Dashboard and Applications Have Separated Authentication Flows - -## Executive Summary - -The dashboard and applications use **two completely separate authentication mechanisms** that operate independently: - -1. **Dashboard**: Uses **NextAuth.js** with JWT-based sessions (30 days) -2. **Applications**: Use **Keycloak SSO** directly via browser cookies - -This separation is why logging out from the dashboard doesn't automatically log you out from applications opened directly in the browser. - ---- - -## Architecture Overview - -### Two Independent Authentication Systems - -``` -┌─────────────────────────────────────────────────────────────┐ -│ AUTHENTICATION LAYERS │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────┐ ┌──────────────────────┐ │ -│ │ DASHBOARD AUTH │ │ APPLICATION AUTH │ │ -│ │ │ │ │ │ -│ │ NextAuth.js │ │ Keycloak SSO │ │ -│ │ (JWT Strategy) │ │ (Cookie-based) │ │ -│ │ │ │ │ │ -│ │ - Session: 30 days │ │ - Session: Variable │ │ -│ │ - Stored in: Cookie │ │ - Stored in: Cookie │ │ -│ │ - Domain: Dashboard │ │ - Domain: Keycloak │ │ -│ │ - Independent │ │ - Independent │ │ -│ └──────────────────────┘ └──────────────────────┘ │ -│ │ │ │ -│ └──────────┬───────────────────┘ │ -│ │ │ -│ ┌───────▼────────┐ │ -│ │ KEYCLOAK │ │ -│ │ (IdP Server) │ │ -│ └────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Why They're Separated - -### 1. Different Authentication Purposes - -**Dashboard Authentication (NextAuth.js)**: -- Purpose: Authenticate the **Next.js dashboard application** -- Method: OAuth 2.0 flow → Get tokens → Store in JWT -- Session Management: NextAuth manages its own session lifecycle -- Storage: Encrypted JWT in HTTP-only cookie on dashboard domain -- Duration: 30 days (configurable in `app/api/auth/options.ts`) - -**Application Authentication (Keycloak SSO)**: -- Purpose: Authenticate **standalone applications** (not embedded in dashboard) -- Method: Direct Keycloak authentication via browser cookies -- Session Management: Keycloak manages SSO session lifecycle -- Storage: Keycloak session cookies on Keycloak domain -- Duration: Configured in Keycloak (typically 30 minutes to a few hours) - -### 2. Different Session Storage Locations - -**Dashboard Session**: -``` -Cookie Name: next-auth.session-token -Domain: dashboard.example.com -Path: / -HttpOnly: Yes -Secure: Yes (if HTTPS) -SameSite: Lax -Content: Encrypted JWT containing: - - accessToken (Keycloak OAuth token) - - refreshToken (Keycloak refresh token) - - idToken (Keycloak ID token) - - User info (id, email, roles, etc.) -``` - -**Application Session**: -``` -Cookie Name: KEYCLOAK_SESSION -Domain: keycloak.example.com (or configured domain) -Path: / -HttpOnly: Yes -Secure: Yes -SameSite: Lax or None (for cross-site) -Content: Keycloak session identifier -``` - -### 3. Different Authentication Flows - -**Dashboard Flow**: -``` -1. User visits dashboard → /signin -2. NextAuth redirects to Keycloak OAuth endpoint -3. Keycloak authenticates user -4. Keycloak redirects back with authorization code -5. NextAuth exchanges code for tokens -6. NextAuth creates JWT session -7. JWT stored in dashboard cookie -8. Dashboard uses JWT for authentication -``` - -**Application Flow** (when opened directly): -``` -1. User visits application directly (not via dashboard) -2. Application checks for Keycloak session cookie -3. If cookie exists → User is authenticated (SSO) -4. If cookie doesn't exist → Redirect to Keycloak login -5. Keycloak authenticates user -6. Keycloak sets session cookie -7. Application uses cookie for authentication -``` - ---- - -## Why Dashboard Logout Doesn't Log Out Applications - -### The Problem - -When you log out from the dashboard: - -1. **Dashboard logout process**: - - Clears NextAuth session cookie (`next-auth.session-token`) - - Calls Keycloak logout endpoint with `id_token_hint` - - Keycloak clears **client session** for dashboard OAuth client - - Keycloak may clear SSO session (if it's the last client session) - -2. **What happens to applications**: - - Applications don't know about dashboard logout - - Applications still have Keycloak SSO session cookie - - Applications continue to work because they use Keycloak cookies, not NextAuth - -### Technical Reasons - -#### Reason 1: Different Cookie Domains - -**Dashboard Cookie**: -- Domain: `dashboard.example.com` -- Cleared when dashboard logs out -- Applications can't access this cookie (different domain) - -**Keycloak SSO Cookie**: -- Domain: `keycloak.example.com` (or configured domain) -- Not cleared by dashboard logout (unless SSO session is cleared) -- Applications can access this cookie (same domain as Keycloak) - -#### Reason 2: Independent Session Lifecycles - -**NextAuth Session**: -- Managed by NextAuth.js -- Lifecycle: Created on login → Valid for 30 days → Cleared on logout -- Independent of Keycloak SSO session - -**Keycloak SSO Session**: -- Managed by Keycloak server -- Lifecycle: Created on login → Valid until timeout or explicit logout → Cleared on logout -- Independent of NextAuth session - -#### Reason 3: Different Authentication Mechanisms - -**Dashboard**: -- Uses OAuth 2.0 tokens (access token, refresh token) -- Tokens stored in NextAuth JWT -- Authentication: Validate JWT → Extract tokens → Use tokens for API calls - -**Applications**: -- Use Keycloak session cookies directly -- No OAuth tokens involved -- Authentication: Check for Keycloak session cookie → If exists, user is authenticated - -#### Reason 4: Keycloak SSO Session Persistence - -**Keycloak maintains two types of sessions**: - -1. **Client Session** (per OAuth client): - - Specific to each OAuth client (dashboard, app1, app2, etc.) - - Cleared when that specific client logs out - - Dashboard logout clears dashboard's client session - -2. **SSO Session** (realm-wide): - - Shared across all clients in the realm - - Persists even after individual client logouts - - Only cleared when: - - All client sessions are logged out - - Explicit SSO session logout - - Session timeout - - Admin API logout - -**When dashboard logs out**: -- Dashboard's client session is cleared ✅ -- SSO session may persist if other applications have active sessions ❌ -- Applications continue to work because SSO session is still valid ❌ - ---- - -## Current Logout Flow Analysis - -### What Happens When You Log Out from Dashboard - -``` -Step 1: User clicks logout in dashboard - ↓ -Step 2: Dashboard calls NextAuth signOut() - → Clears: next-auth.session-token cookie - → Clears: Dashboard's NextAuth session - ↓ -Step 3: Dashboard calls /api/auth/end-sso-session - → Uses Keycloak Admin API - → Calls: adminClient.users.logout({ id: userId }) - → Clears: All client sessions for user - → May clear: SSO session (if it's the last client session) - ↓ -Step 4: Dashboard redirects to Keycloak logout endpoint - → URL: ${KEYCLOAK_ISSUER}/protocol/openid-connect/logout - → Parameters: id_token_hint, post_logout_redirect_uri - → Clears: Dashboard's client session - → May clear: SSO session (if it's the last client session) - ↓ -Step 5: Keycloak redirects back to /signin?logout=true - → Dashboard shows logout message -``` - -### What Happens to Applications - -``` -Applications opened directly in browser: - ↓ -Step 1: Application checks for Keycloak session cookie - → Cookie: KEYCLOAK_SESSION - → Domain: keycloak.example.com - ↓ -Step 2: If SSO session still exists: - → Application finds valid SSO session cookie ✅ - → Application authenticates user automatically ✅ - → User remains logged in ❌ - ↓ -Step 3: If SSO session was cleared: - → Application doesn't find session cookie ✅ - → Application redirects to Keycloak login ✅ - → User must log in again ✅ -``` - -### Why Applications Stay Logged In - -**Scenario 1: SSO Session Persists** -- Dashboard logout clears client sessions -- But SSO session cookie still exists -- Applications check SSO session cookie → Still valid → User stays logged in - -**Scenario 2: Other Applications Have Active Sessions** -- If other applications are open in other tabs/windows -- They have active client sessions -- Keycloak won't clear SSO session (because other clients are still active) -- All applications stay logged in - -**Scenario 3: Cookie Domain Mismatch** -- Dashboard tries to clear Keycloak cookies client-side -- But cookies are on different domain (keycloak.example.com) -- Browser security prevents clearing cross-domain cookies -- Applications keep their cookies → Stay logged in - ---- - -## Why This Architecture Exists - -### Historical/Design Reasons - -1. **Legacy Applications**: - - Applications may have existed before the dashboard - - They were designed to use Keycloak directly - - Dashboard was added later as a wrapper/portal - -2. **Separation of Concerns**: - - Dashboard: Portal/aggregator (doesn't need to know about app internals) - - Applications: Standalone services (don't depend on dashboard) - -3. **Flexibility**: - - Applications can be accessed directly (not just via dashboard) - - Applications can be used independently - - Dashboard is optional, not required - -4. **SSO Design**: - - Keycloak SSO is designed to work across multiple applications - - Logging out from one app shouldn't log out from all apps - - This is by design for SSO functionality - -### Technical Constraints - -1. **Cookie Security**: - - Browsers prevent cross-domain cookie access - - Dashboard can't directly clear Keycloak cookies (different domain) - - Must use Keycloak logout endpoint or Admin API - -2. **Stateless vs Stateful**: - - NextAuth: Stateless (JWT, no server-side session) - - Keycloak: Stateful (server-side session, cookies) - -3. **OAuth vs Direct Authentication**: - - Dashboard: Uses OAuth 2.0 (tokens) - - Applications: Use direct Keycloak authentication (cookies) - ---- - -## What Would Be Needed for Unified Logout - -To make dashboard logout also log out applications, you would need: - -### Option 1: Keycloak Front-Channel Logout (Recommended) -- Configure all applications to participate in Front-Channel Logout -- When dashboard logs out, Keycloak notifies all registered applications -- Applications receive logout notification and clear their sessions -- **Requires**: Keycloak configuration + Application support - -### Option 2: Keycloak Single Logout (SLO) -- Configure all applications to participate in SLO -- When one application logs out, all applications are logged out -- **Requires**: Keycloak configuration + Application support - -### Option 3: Clear SSO Session Explicitly -- Use Keycloak Admin API to end SSO session -- This clears the realm-wide SSO session -- All applications lose their authentication -- **Current Implementation**: Partially implemented (`/api/auth/end-sso-session`) -- **Issue**: May not clear SSO session cookie if other clients are active - -### Option 4: Application Logout Endpoints -- Each application exposes a logout endpoint -- Dashboard calls all application logout endpoints -- Applications clear their own sessions -- **Requires**: Application modifications + Dashboard coordination - ---- - -## Summary - -### Why They're Separated - -1. **Different purposes**: Dashboard is a portal, applications are standalone services -2. **Different storage**: Dashboard uses NextAuth JWT, applications use Keycloak cookies -3. **Different domains**: Cookies are on different domains (security prevents cross-domain access) -4. **Different lifecycles**: NextAuth session (30 days) vs Keycloak SSO session (variable) -5. **SSO design**: Keycloak SSO is designed to persist across client logouts - -### Why Dashboard Logout Doesn't Log Out Applications - -1. **SSO session persists**: Keycloak SSO session may not be cleared -2. **Other active sessions**: If other applications are open, SSO session stays active -3. **Cookie domain**: Dashboard can't directly clear Keycloak cookies (different domain) -4. **Independent mechanisms**: Applications don't know about NextAuth session state - -### The Solution - -To achieve unified logout, you need to: -- Configure Keycloak Front-Channel Logout or SLO -- Ensure all applications participate in logout notifications -- Or use Admin API to explicitly end SSO session (current implementation attempts this) - -The current implementation (`/api/auth/end-sso-session`) tries to clear the SSO session, but it may not work if: -- Other applications have active sessions -- SSO session cookie is on a different domain -- Keycloak configuration prevents SSO session clearing - diff --git a/SESSION_CALLBACK_LOGGING_IMPACT_ANALYSIS.md b/SESSION_CALLBACK_LOGGING_IMPACT_ANALYSIS.md deleted file mode 100644 index a6a76ba9..00000000 --- a/SESSION_CALLBACK_LOGGING_IMPACT_ANALYSIS.md +++ /dev/null @@ -1,335 +0,0 @@ -# Session Callback Logging - Impact Analysis - -**Date**: 2026-01-01 -**Purpose**: Analyze the impact of reducing session callback logging on the multi-stack architecture - ---- - -## 🏗️ Architecture Overview - -### Stack Components -1. **Next.js Dashboard** (this application) -2. **Keycloak** (SSO/Authentication provider) -3. **MinIO** (Object storage for files) -4. **External Services** (Leantime, Rocket.Chat, News API, etc.) - -### Integration Points -- **Keycloak**: OAuth2/OIDC provider, session tokens, role extraction -- **MinIO**: File storage (mission logos, attachments), S3-compatible API -- **External APIs**: All require authenticated session - ---- - -## 📋 Current Session Callback Logging - -### What's Being Logged -```typescript -// Lines 407-472 in app/api/auth/options.ts -- === SESSION CALLBACK START === -- Token error status -- Access token presence -- Refresh token presence -- Token roles -- Token sub (user ID) -- Token email -- Token name -- Token username -- User roles for session -- Creating session user object -- Setting session tokens -- ✅ Session created successfully -- Session user details -- === SESSION CALLBACK END === -``` - -### Why It Was Added -**Historical Context** (from `DEBUG_502_CALLBACK.md`): -- Added specifically to debug **502 errors** with Keycloak callbacks -- Critical for diagnosing authentication failures -- Helps identify when session callback doesn't execute -- Essential for troubleshooting SSO flow issues - ---- - -## 🔍 Impact Analysis - -### 1. Keycloak Integration Impact - -**Dependencies**: -- ✅ **No functional impact**: Logging doesn't affect Keycloak authentication -- ⚠️ **Debugging impact**: Removing logs makes troubleshooting harder -- ✅ **Error logging preserved**: Critical errors still logged - -**Keycloak Flow**: -``` -1. User authenticates → Keycloak -2. Keycloak redirects → Next.js callback -3. JWT callback extracts tokens -4. Session callback builds session ← LOGGING HERE -5. Session used for all API calls -``` - -**Recommendation**: -- Keep error logging (always) -- Make success logging conditional (DEBUG_SESSION flag) - ---- - -### 2. MinIO Integration Impact - -**Dependencies**: -- ✅ **No direct dependency**: MinIO doesn't use session callback logs -- ✅ **Uses session for auth**: Session object used to verify user permissions -- ✅ **No impact**: Logging changes won't affect MinIO operations - -**MinIO Flow**: -``` -1. API route calls getServerSession() -2. Session callback executes (builds session) -3. Session used to verify user authentication -4. MinIO operations proceed with authenticated user -``` - -**Recommendation**: -- ✅ **Safe to reduce logging**: No impact on MinIO functionality - ---- - -### 3. External Services Impact - -**Services**: -- Leantime (project management) -- Rocket.Chat (messaging) -- News API -- Email/IMAP - -**Dependencies**: -- ✅ **No functional impact**: Services don't read logs -- ✅ **Session still created**: Logging doesn't affect session creation -- ✅ **Authentication works**: Session object still valid - -**Recommendation**: -- ✅ **Safe to reduce logging**: No impact on external services - ---- - -### 4. Monitoring & Debugging Impact - -**Current Usage**: -- Debugging 502 errors (Keycloak callbacks) -- Troubleshooting authentication issues -- Monitoring session creation frequency -- Identifying session callback failures - -**Impact of Reducing Logging**: -- ⚠️ **Harder to debug**: Less visibility into session creation -- ✅ **Still debuggable**: Error logging preserved -- ✅ **Can enable on-demand**: DEBUG_SESSION flag for troubleshooting - -**Recommendation**: -- Use conditional logging with DEBUG_SESSION flag -- Keep error logging always enabled -- Document how to enable debug logging - ---- - -## ✅ Safe Implementation Strategy - -### Phase 1: Conditional Logging (Recommended) - -**Approach**: Make success logging conditional, keep error logging always - -```typescript -async session({ session, token }) { - try { - // Always log errors - if (token.error) { - console.error("❌ Session callback error:", token.error); - } - - // Conditional verbose logging - const DEBUG_SESSION = process.env.DEBUG_SESSION === 'true' || - process.env.NODE_ENV === 'development'; - - if (DEBUG_SESSION) { - console.log('=== SESSION CALLBACK START ==='); - console.log('Token error:', token.error); - console.log('Has accessToken:', !!token.accessToken); - // ... rest of verbose logging - } - - // Always log critical errors - if (token.error === "SessionNotActive" || - token.error === "NoRefreshToken" || - !token.accessToken || - !token.refreshToken) { - console.log("❌ Session invalidated or tokens missing", { - error: token.error, - hasAccessToken: !!token.accessToken, - hasRefreshToken: !!token.refreshToken - }); - return null as any; - } - - // ... rest of callback logic - - if (DEBUG_SESSION) { - console.log('✅ Session created successfully'); - console.log('Session user id:', session.user.id); - console.log('=== SESSION CALLBACK END ==='); - } - - return session; - } catch (error) { - // Always log critical errors - console.error('❌❌❌ CRITICAL ERROR IN SESSION CALLBACK ❌❌❌'); - console.error('Error:', error); - throw error; - } -} -``` - -**Benefits**: -- ✅ Production: Minimal logging (errors only) -- ✅ Development: Full logging for debugging -- ✅ On-demand: Enable with DEBUG_SESSION=true -- ✅ No functional impact - ---- - -### Phase 2: Environment-Based Logging - -**Alternative**: Use NODE_ENV - -```typescript -const isDevelopment = process.env.NODE_ENV === 'development'; - -if (isDevelopment || token.error) { - // Verbose logging -} -``` - -**Benefits**: -- ✅ Simple implementation -- ✅ Automatic in development -- ⚠️ Less flexible than DEBUG_SESSION flag - ---- - -## 🎯 Recommended Approach - -### Option 1: DEBUG_SESSION Flag (Best) - -**Implementation**: -- Add `DEBUG_SESSION` environment variable -- Default: `false` (minimal logging) -- Set to `true` when debugging needed - -**Usage**: -```bash -# Production (minimal logging) -DEBUG_SESSION=false npm start - -# Debugging (verbose logging) -DEBUG_SESSION=true npm start -``` - -**Pros**: -- ✅ Flexible (can enable on-demand) -- ✅ Production-friendly (minimal logs) -- ✅ Debug-friendly (full logs when needed) -- ✅ No code changes needed to toggle - -**Cons**: -- ⚠️ Requires environment variable management - ---- - -### Option 2: NODE_ENV Based (Simpler) - -**Implementation**: -- Use `NODE_ENV === 'development'` for verbose logging -- Always log errors - -**Pros**: -- ✅ Simple (no new env vars) -- ✅ Automatic (works with existing setup) - -**Cons**: -- ⚠️ Less flexible (can't enable in production easily) - ---- - -## 📊 Risk Assessment - -| Risk | Impact | Mitigation | -|------|--------|------------| -| **Lost debugging capability** | Medium | Keep error logging, add DEBUG_SESSION flag | -| **Harder to troubleshoot 502 errors** | Medium | Document how to enable debug logging | -| **Performance impact** | Low | Logging overhead is minimal | -| **Functional impact** | None | Logging doesn't affect functionality | - ---- - -## ✅ Final Recommendation - -### Implementation Plan - -1. **Keep Error Logging Always** ✅ - - Critical errors always logged - - Session invalidation always logged - - Exception handling always logged - -2. **Make Success Logging Conditional** ✅ - - Use `DEBUG_SESSION` environment variable - - Default: `false` (production-friendly) - - Can enable: `DEBUG_SESSION=true` (debugging) - -3. **Document Debugging Process** ✅ - - Add to README or troubleshooting guide - - Explain when to enable DEBUG_SESSION - - Document what logs to look for - -4. **Test in Staging** ✅ - - Verify error logging still works - - Test with DEBUG_SESSION=true - - Test with DEBUG_SESSION=false - ---- - -## 🔧 Implementation Checklist - -- [ ] Update `app/api/auth/options.ts` with conditional logging -- [ ] Add `DEBUG_SESSION` to environment variable documentation -- [ ] Test error logging (should always work) -- [ ] Test success logging with DEBUG_SESSION=true -- [ ] Test success logging with DEBUG_SESSION=false -- [ ] Verify Keycloak authentication still works -- [ ] Verify MinIO operations still work -- [ ] Verify external services still work -- [ ] Update troubleshooting documentation - ---- - -## 📝 Summary - -**Impact Level**: 🟢 **LOW RISK** - -**Key Findings**: -1. ✅ No functional impact on Keycloak, MinIO, or external services -2. ✅ Logging was added for debugging, not functionality -3. ✅ Error logging preserved (critical for troubleshooting) -4. ✅ Conditional logging provides flexibility - -**Recommendation**: -- ✅ **Proceed with conditional logging** -- ✅ **Use DEBUG_SESSION flag for flexibility** -- ✅ **Keep error logging always enabled** - -**Confidence**: 🟢 **HIGH** - Safe to implement - ---- - -**Generated**: 2026-01-01 -**Next Step**: Implement conditional logging in `app/api/auth/options.ts` - diff --git a/SESSION_DURATION_SECURITY_ANALYSIS.md b/SESSION_DURATION_SECURITY_ANALYSIS.md deleted file mode 100644 index f6636bbe..00000000 --- a/SESSION_DURATION_SECURITY_ANALYSIS.md +++ /dev/null @@ -1,233 +0,0 @@ -# NextAuth Session Duration: 30 Days vs 4 Hours - Security Analysis - -## Current Configuration - -**Current Setting** (`app/api/auth/options.ts:190`): -```typescript -session: { - strategy: "jwt", - maxAge: 30 * 24 * 60 * 60, // 30 days (2,592,000 seconds) -} -``` - -**Proposed Setting**: -```typescript -session: { - strategy: "jwt", - maxAge: 4 * 60 * 60, // 4 hours (14,400 seconds) -} -``` - ---- - -## Security Analysis - -### ✅ **Why 4 Hours is Better for Security** - -1. **Reduced Attack Window**: - - **30 days**: If session is compromised, attacker has 30 days of access - - **4 hours**: If session is compromised, attacker has maximum 4 hours of access - - **Risk Reduction**: 99.4% reduction in maximum exposure time - -2. **Industry Best Practices**: - - **NIST Guidelines**: Recommend session timeouts of 2-8 hours for high-security applications - - **OWASP**: Recommends session timeouts based on risk level (typically 2-8 hours) - - **Common Practice**: Most enterprise applications use 4-8 hour sessions - -3. **Device Security**: - - **30 days**: Device left unattended = 30 days of potential unauthorized access - - **4 hours**: Device left unattended = maximum 4 hours of potential access - - **Better for**: Shared devices, public computers, unattended workstations - -4. **Compliance**: - - Many security standards (ISO 27001, SOC 2) require reasonable session timeouts - - 30 days is often considered too long for compliance - - 4 hours aligns better with security compliance requirements - -5. **Stolen Session Cookie**: - - If session cookie is stolen (XSS, MITM), shorter duration limits damage - - 4 hours gives attacker limited time to exploit - - 30 days gives attacker extensive time to exploit - -### ⚠️ **Considerations & Trade-offs** - -1. **User Experience Impact**: - - **30 days**: Users rarely need to re-authenticate (convenient) - - **4 hours**: Users need to re-authenticate every 4 hours (less convenient) - - **Impact**: Moderate - users will need to log in more frequently - -2. **Token Refresh Behavior**: - - **Good News**: Your code already handles token refresh automatically - - **How it works**: - - When NextAuth session expires (4 hours), JWT callback runs - - If `accessToken` is expired, it calls `refreshAccessToken()` - - Uses `refreshToken` to get new tokens from Keycloak - - Session is automatically renewed (if refresh token is still valid) - - **Result**: Users may not notice the 4-hour expiration if they're active - -3. **Keycloak Refresh Token Lifetime**: - - **Important**: Keycloak refresh tokens typically last 7-30 days - - **What this means**: - - NextAuth session expires after 4 hours - - But refresh token is still valid (e.g., 7 days) - - NextAuth automatically refreshes tokens - - User stays logged in seamlessly (if active) - - **Only expires if**: User is inactive for longer than refresh token lifetime - -4. **Keycloak Session Alignment**: - - **Current Issue**: Keycloak sessions typically expire in 30 minutes to a few hours - - **With 4-hour NextAuth session**: - - Better alignment with Keycloak session timeouts - - Reduces session mismatch issues - - Iframe applications will have more consistent session state - ---- - -## How It Will Work - -### Session Lifecycle with 4-Hour maxAge - -``` -User logs in - ↓ -NextAuth creates JWT session (expires in 4 hours) - ↓ -User is active for 2 hours - ↓ -User makes request → NextAuth checks session - ↓ -Session still valid (< 4 hours) → Continue - ↓ -User is active for 3 hours - ↓ -User makes request → NextAuth checks session - ↓ -Session still valid (< 4 hours) → Continue - ↓ -User is active for 4.5 hours (session expired) - ↓ -User makes request → NextAuth checks session - ↓ -Session expired → JWT callback runs - ↓ -Checks accessToken expiration - ↓ -If accessToken expired → Calls refreshAccessToken() - ↓ -Uses refreshToken to get new tokens from Keycloak - ↓ -If refreshToken still valid → New session created (another 4 hours) - ↓ -User continues seamlessly (no re-authentication needed) - ↓ -If refreshToken expired → User must re-authenticate -``` - -### When User Must Re-authenticate - -**User must re-authenticate if**: -1. **Inactive for longer than refresh token lifetime** (typically 7-30 days) -2. **Refresh token is revoked** (logout, admin action, security event) -3. **Keycloak session is invalidated** (logout from another application) - -**User does NOT need to re-authenticate if**: -1. **Active within refresh token lifetime** (automatic token refresh) -2. **Session expires but refresh token is valid** (automatic renewal) - ---- - -## Recommendations - -### ✅ **Recommendation: Implement 4-Hour Session** - -**Reasons**: -1. ✅ **Significantly better security** (99.4% reduction in exposure window) -2. ✅ **Aligns with industry best practices** (NIST, OWASP) -3. ✅ **Better compliance** (meets security standards) -4. ✅ **Better alignment with Keycloak sessions** -5. ✅ **Minimal UX impact** (automatic token refresh handles renewal) -6. ✅ **Code already supports it** (token refresh mechanism exists) - -### ⚠️ **Important Considerations** - -1. **Verify Keycloak Refresh Token Lifetime**: - - Check Keycloak configuration for refresh token lifetime - - Ensure it's longer than 4 hours (typically 7-30 days) - - If shorter, users will need to re-authenticate frequently - -2. **Monitor User Experience**: - - Track how often users need to re-authenticate - - If too frequent, consider increasing to 6-8 hours - - Balance security with usability - -3. **Consider Activity-Based Extension**: - - Current implementation: Fixed 4-hour expiration - - Alternative: Extend session on activity (sliding window) - - Requires additional implementation (activity tracking) - -4. **Keycloak Session Configuration**: - - Consider aligning Keycloak SSO session timeout with NextAuth - - Or ensure Keycloak session is longer than NextAuth session - - Prevents session mismatch issues - -### 📋 **Implementation Checklist** - -Before implementing: - -- [ ] Verify Keycloak refresh token lifetime (should be > 4 hours) -- [ ] Test token refresh flow with 4-hour session -- [ ] Monitor user re-authentication frequency -- [ ] Consider user feedback on session duration -- [ ] Document the change for users (if needed) -- [ ] Update security documentation - ---- - -## Comparison Table - -| Aspect | 30 Days | 4 Hours | Winner | -|--------|---------|---------|--------| -| **Security** | Low (long exposure window) | High (short exposure window) | ✅ 4 Hours | -| **User Convenience** | High (rare re-authentication) | Medium (automatic refresh) | ✅ 30 Days | -| **Compliance** | Poor (too long) | Good (meets standards) | ✅ 4 Hours | -| **Risk Reduction** | Low | High (99.4% reduction) | ✅ 4 Hours | -| **Keycloak Alignment** | Poor (mismatch) | Good (better alignment) | ✅ 4 Hours | -| **Token Refresh** | Works | Works (same mechanism) | ✅ Tie | - ---- - -## Conclusion - -**Recommendation: Change to 4 hours** - -**Why**: -- Significantly better security posture -- Aligns with industry best practices -- Better compliance with security standards -- Minimal UX impact (automatic token refresh) -- Better alignment with Keycloak session timeouts -- Code already supports it - -**Implementation**: -- Simple change: `maxAge: 4 * 60 * 60` -- No code changes needed (token refresh already works) -- Monitor user experience and adjust if needed - -**Alternative Consideration**: -- If 4 hours is too aggressive, consider 6-8 hours as a middle ground -- Still provides significant security improvement over 30 days -- Better user experience than 4 hours - ---- - -## Final Verdict - -**✅ Yes, change to 4 hours** - This is a good security practice that: -- Significantly reduces security risk -- Aligns with industry standards -- Has minimal UX impact (automatic refresh) -- Works with existing code -- Better aligns with Keycloak sessions - -The only trade-off is slightly more frequent re-authentication for inactive users, but this is a reasonable security trade-off. - diff --git a/SSO_FLOW_ANALYSIS.md b/SSO_FLOW_ANALYSIS.md deleted file mode 100644 index 4d296177..00000000 --- a/SSO_FLOW_ANALYSIS.md +++ /dev/null @@ -1,250 +0,0 @@ -# SSO Flow Analysis - Keycloak External Logout Issue - -## Current Flow Trace - -### Scenario: User logs out from Keycloak directly, then accesses dashboard - -**Step-by-step flow:** - -1. **Initial State (Before Keycloak Logout)** - - User is logged into Dashboard via NextAuth - - NextAuth JWT contains: - - `accessToken`: Valid Keycloak OAuth token - - `refreshToken`: Valid Keycloak refresh token - - `idToken`: Valid Keycloak ID token - - Keycloak session cookies are set in browser - - Iframe applications can authenticate via Keycloak cookies - -2. **User Logs Out from Keycloak Directly (External Application)** - - External application calls: `POST /realms/{realm}/protocol/openid-connect/logout` - - Keycloak invalidates: - - ✅ Keycloak session cookies (cleared) - - ✅ Keycloak refresh token (invalidated) - - ✅ Keycloak access token (invalidated) - - ❌ **NextAuth JWT still contains old tokens** (NextAuth doesn't know about logout) - - ❌ **NextAuth session cookie still valid** (30-day expiration) - -3. **User Accesses Dashboard** - - Browser sends NextAuth session cookie - - NextAuth decrypts JWT - - JWT contains old (now invalid) tokens - - **Token expiration check**: `Date.now() < (token.accessTokenExpires as number) * 1000` - - If token hasn't expired yet (by timestamp), NextAuth returns existing token - - **Problem**: Token is invalid in Keycloak, but NextAuth doesn't know yet - -4. **User Navigates to Iframe Application** - - `ResponsiveIframe` component mounts - - `useEffect` triggers: `refreshSession()` - - Calls: `GET /api/auth/refresh-keycloak-session` - -5. **Refresh Endpoint Execution** - ``` - GET /api/auth/refresh-keycloak-session - → getServerSession(authOptions) - → Reads NextAuth JWT from cookie - → JWT contains old refreshToken (invalid) - → Calls Keycloak: POST /token with old refreshToken - → Keycloak responds: { error: 'invalid_grant', error_description: 'Token is not active' } - → Returns 401 with SessionInvalidated error - ``` - -6. **ResponsiveIframe Handles Error** - - Detects `SessionInvalidated` error - - Redirects to `/signin` - - User signs in again - - Gets NEW tokens from Keycloak - -7. **User Returns to Iframe (After Re-login)** - - **Problem**: If NextAuth JWT callback hasn't run yet, it might still have old tokens - - OR: The new session is created, but iframe component might be using cached session - - OR: The refresh endpoint is called again before new session is fully established - -## Root Cause Analysis - -### Issue 1: Stale Token Detection - -**Problem**: NextAuth only tries to refresh tokens when they're expired (by timestamp). If a token is invalidated externally (Keycloak logout), NextAuth won't know until it tries to refresh. - -**Current Flow**: -``` -JWT Callback: - if (Date.now() < token.accessTokenExpires * 1000) { - return token; // Returns stale token without checking Keycloak - } - // Only refreshes if expired by timestamp -``` - -**What Should Happen**: -- When accessing iframe, we proactively refresh to validate token -- But if refresh fails, we need to clear the NextAuth session immediately - -### Issue 2: Session Invalidation Timing - -**Problem**: When refresh fails: -1. Refresh endpoint returns `SessionInvalidated` -2. ResponsiveIframe redirects to `/signin` -3. User signs in, gets new tokens -4. **But**: NextAuth JWT might still have old tokens cached until next JWT callback execution - -**Current Behavior**: -- Redirect to signin happens -- User re-authenticates -- New session is created -- But old session might still be in browser cache/cookies - -### Issue 3: Infinite Redirect Loop Potential - -**Problem**: If the refresh endpoint keeps failing: -- ResponsiveIframe redirects to `/signin` -- User signs in -- Returns to iframe -- Refresh endpoint called again -- If new session isn't fully established, it might still use old tokens -- Loop continues - -## Current Code Flow - -### ResponsiveIframe Component Flow - -```typescript -1. Component mounts with session -2. useEffect triggers refreshSession() -3. Calls GET /api/auth/refresh-keycloak-session -4. If 401 + SessionInvalidated: - → window.location.href = '/signin' - → User redirected -5. User signs in again -6. Returns to iframe page -7. Component mounts again -8. useEffect triggers refreshSession() again -9. If session still has old tokens → fails again -``` - -### Refresh Endpoint Flow - -```typescript -GET /api/auth/refresh-keycloak-session -1. getServerSession(authOptions) - → Reads JWT from cookie - → JWT callback runs - → If token expired: refreshAccessToken() - → If token not expired: returns existing token (might be invalid!) -2. Uses session.refreshToken -3. Calls Keycloak refresh endpoint -4. If invalid_grant: Returns SessionInvalidated -``` - -### JWT Callback Flow - -```typescript -async jwt({ token, account, profile }) { - // Initial login: account & profile present - if (account && profile) { - // Store tokens - } - - // Subsequent requests - else if (token.accessToken) { - // Check expiration - if (Date.now() < token.accessTokenExpires * 1000) { - return token; // ⚠️ Returns token without validating with Keycloak - } - - // Only refreshes if expired by timestamp - return refreshAccessToken(token); - } -} -``` - -## The Problem - -**Key Issue**: NextAuth JWT callback only checks token expiration by timestamp. It doesn't validate that the token is still valid in Keycloak. So: - -1. User logs out from Keycloak → Token invalidated -2. NextAuth JWT still has token (not expired by timestamp) -3. JWT callback returns existing token (assumes it's valid) -4. Refresh endpoint tries to use invalid refresh token -5. Fails, redirects to signin -6. User signs in, but if JWT callback hasn't run with new account, might still have old token - -## Why It Gets Stuck - -Looking at the logs: -``` -Failed to refresh Keycloak session: { error: 'invalid_grant', error_description: 'Token is not active' } -GET /api/auth/refresh-keycloak-session 401 -→ Redirects to /signin -→ User signs in -→ Returns to iframe -→ refresh-keycloak-session called again -→ Still fails (401) -``` - -**Possible reasons**: -1. **Session not fully updated**: After signin, NextAuth creates new session, but refresh endpoint might be reading old session from cookie before it's updated -2. **Token not refreshed in JWT**: The new tokens from signin might not be stored in JWT yet when refresh endpoint is called -3. **Cookie caching**: Browser might be sending old session cookie -4. **Race condition**: Refresh endpoint called before new session is established - -## Recommendations (Without Code Changes) - -### 1. Check Session State After Signin - -After user signs in and is redirected back: -- Verify that `getServerSession()` returns new session with valid tokens -- Check that JWT callback has run and stored new tokens -- Ensure session cookie is updated in browser - -### 2. Add Delay/Retry Logic - -In ResponsiveIframe: -- After redirect from signin, wait a moment before calling refresh endpoint -- Or check if session has been updated before calling refresh -- Add retry logic with exponential backoff - -### 3. Validate Token Before Using - -In refresh endpoint: -- Before using refreshToken, validate that accessToken is still valid -- Or check token age - if token is old, force refresh even if not expired - -### 4. Clear Session on Invalid Token - -When refresh fails with invalid_grant: -- Don't just redirect - also clear NextAuth session cookie -- Force complete re-authentication -- Ensure old session is completely removed - -### 5. Check Keycloak Session Status - -Before calling refresh endpoint: -- Check if Keycloak session is still active -- Use Keycloak's userinfo endpoint to validate access token -- Only refresh if token is actually invalid - -## Current Behavior Summary - -**What's Happening**: -1. ✅ User logs out from Keycloak → Keycloak invalidates tokens -2. ✅ User accesses dashboard → NextAuth still has old tokens (not expired by timestamp) -3. ✅ User goes to iframe → Refresh endpoint called -4. ✅ Refresh fails → Detects invalid token -5. ✅ Redirects to signin → User re-authenticates -6. ⚠️ **Issue 1**: Storage initialization fails during signin (`createUserFolderStructure` not exported) -7. ⚠️ **Issue 2**: After re-authentication, refresh endpoint might still be using old session -8. ⚠️ **Result**: Gets stuck in redirect loop or keeps failing - -**Root Cause**: NextAuth doesn't proactively validate tokens with Keycloak. It only checks expiration timestamps. When tokens are invalidated externally, NextAuth doesn't know until it tries to use them. - -**Additional Issue Confirmed**: -- Storage initialization fails during signin process -- Error: `createUserFolderStructure is not a function` -- This prevents complete signin initialization -- May contribute to session not being fully established - ---- - -**Analysis Date**: 2024 -**Status**: Issue Identified -**Next Steps**: Implement proactive token validation or improve session invalidation handling - diff --git a/SSO_FLOW_CONFIRMED.md b/SSO_FLOW_CONFIRMED.md deleted file mode 100644 index 207e902a..00000000 --- a/SSO_FLOW_CONFIRMED.md +++ /dev/null @@ -1,207 +0,0 @@ -# SSO Flow Analysis - Confirmed Issues - -## Browser Console Evidence - -Based on the browser console error provided, here's what's happening: - -### Confirmed Flow - -1. **User logs out from Keycloak directly** (external application) - - Keycloak invalidates all tokens and session - -2. **User accesses Dashboard** - - NextAuth session still exists (30-day expiration) - - JWT contains old, now-invalid tokens - - Token expiration check: `Date.now() < token.accessTokenExpires * 1000` - - If token hasn't expired by timestamp → JWT callback returns old token - - **Problem**: Token is invalid in Keycloak, but NextAuth doesn't validate it - -3. **User navigates to iframe application** - - `ResponsiveIframe` component mounts - - Calls `GET /api/auth/refresh-keycloak-session` - - Refresh endpoint uses old `refreshToken` from session - - Keycloak responds: `{ error: 'invalid_grant', error_description: 'Token is not active' }` - - Returns 401 with `SessionInvalidated` error - -4. **Redirect to Signin** - - `ResponsiveIframe` detects `SessionInvalidated` - - Redirects: `window.location.href = '/signin'` - - User lands on signin page - -5. **Signin Process Starts** - - `app/signin/page.tsx` detects unauthenticated status - - Triggers: `signIn("keycloak", { callbackUrl: "/" })` - - User authenticates with Keycloak - - Gets NEW tokens from Keycloak - - NextAuth callback stores new tokens in JWT - -6. **Storage Initialization Fails** ⚠️ **CONFIRMED ISSUE** - - Signin page detects session available - - Calls: `POST /api/storage/init` - - Storage endpoint tries to call: `createUserFolderStructure(session.user.id)` - - **Error**: `createUserFolderStructure is not a function` - - Storage initialization fails - - Signin page shows "Échec de l'initialisation" - - **Impact**: User might not be fully signed in, or session might not be complete - -7. **User Returns to Iframe** (After Signin) - - Navigates to iframe application again - - `ResponsiveIframe` component mounts - - Calls refresh endpoint again - - **If storage init failed**: Session might not be fully established - - **If new session not ready**: Might still use old tokens - - Refresh fails again → Redirects to signin → Loop - -## Confirmed Issues - -### Issue 1: Storage Initialization Failure ✅ CONFIRMED - -**Error**: `createUserFolderStructure is not a function` - -**Location**: `app/api/storage/init/route.ts:16` - -**Impact on SSO Flow**: -- Storage initialization is part of signin process -- If it fails, signin might not complete properly -- Session might not be fully established -- When user tries to access iframe, refresh endpoint might fail because: - - Session not complete - - Or still using old tokens if new session wasn't saved - -**Evidence from Browser**: -``` -Failed to initialize storage: "{\"error\":\"Failed to initialize storage\",\"details\":\"(0 , _lib_s3__WEBPACK_IMPORTED_MODULE_3__.createUserFolderStructure) is not a function\"}" -``` - -### Issue 2: Stale Token in NextAuth JWT ✅ CONFIRMED - -**Problem**: When Keycloak session is invalidated externally: -- NextAuth JWT still contains old tokens -- JWT callback only checks expiration timestamp -- Doesn't validate with Keycloak that token is still valid -- Returns stale token until expiration timestamp is reached - -**Evidence from Terminal**: -``` -Failed to refresh Keycloak session: { error: 'invalid_grant', error_description: 'Token is not active' } -GET /api/auth/refresh-keycloak-session 401 -``` - -### Issue 3: Race Condition After Re-authentication ✅ CONFIRMED - -**Problem**: After user signs in again: -- New session is created -- But refresh endpoint might be called before: - - JWT callback has run with new account - - New tokens are stored in JWT - - Session cookie is updated in browser -- Result: Refresh endpoint still uses old tokens - -**Evidence**: Multiple failed refresh attempts after signin - -## Complete Flow Diagram (Confirmed) - -``` -1. User logs out from Keycloak (external) - ↓ -2. Keycloak invalidates: - - Session cookies ✅ - - Refresh token ✅ - - Access token ✅ - ↓ -3. User accesses Dashboard - - NextAuth JWT has old tokens (not expired by timestamp) - - JWT callback returns old token (doesn't validate with Keycloak) - ↓ -4. User navigates to iframe - - ResponsiveIframe calls refresh endpoint - - Uses old refreshToken from session - ↓ -5. Keycloak rejects: "Token is not active" - ↓ -6. Refresh endpoint returns 401 SessionInvalidated - ↓ -7. Redirect to /signin - ↓ -8. User authenticates with Keycloak - - Gets NEW tokens - - NextAuth stores new tokens in JWT - ↓ -9. Storage initialization called - - ⚠️ FAILS: createUserFolderStructure not found - - Signin process incomplete - ↓ -10. User navigates to iframe again - - Refresh endpoint called - - ⚠️ Might still use old tokens (if new session not ready) - - OR: Session incomplete due to storage init failure - - Fails again → Redirects to signin - ↓ -11. LOOP or stuck state -``` - -## Root Causes (Confirmed) - -1. **No Proactive Token Validation** - - NextAuth only checks expiration timestamps - - Doesn't validate tokens with Keycloak - - Stale tokens remain in JWT until timestamp expiration - -2. **Storage Initialization Failure** - - Missing function: `createUserFolderStructure` - - Prevents complete signin initialization - - May cause session to be incomplete - -3. **Race Condition** - - Refresh endpoint called before new session fully established - - Browser might send old session cookie - - JWT callback might not have run yet with new account - -4. **No Session Invalidation on External Logout** - - When Keycloak session invalidated externally - - NextAuth doesn't know about it - - Continues using invalid tokens - -## Impact on User Experience - -**What User Sees**: -1. Logs out from Keycloak (external app) -2. Accesses Dashboard → Still logged in (NextAuth session valid) -3. Tries to access iframe application -4. Gets redirected to signin -5. Signs in again -6. Storage initialization fails (error message) -7. Tries to access iframe again -8. Gets redirected to signin again -9. **Stuck in loop or keeps getting disconnected** - -## Recommendations - -### Immediate Fixes Needed - -1. **Fix Storage Initialization** - - Export `createUserFolderStructure` from `lib/s3.ts` - - Or remove storage init from signin flow if not critical - - Prevents signin from failing - -2. **Proactive Token Validation** - - Before using tokens, validate with Keycloak - - Use Keycloak's userinfo endpoint to check token validity - - Clear session if token invalid - -3. **Session Invalidation on Refresh Failure** - - When refresh fails with invalid_grant - - Immediately clear NextAuth session cookie - - Force complete re-authentication - -4. **Delay Refresh After Signin** - - After redirect from signin, wait for session to be established - - Check session status before calling refresh endpoint - - Add retry logic with backoff - ---- - -**Analysis Date**: 2024 -**Status**: Issues Confirmed -**Evidence**: Browser console + Terminal logs - diff --git a/STACK_QUALITY_AND_FLOW_ANALYSIS.md b/STACK_QUALITY_AND_FLOW_ANALYSIS.md deleted file mode 100644 index c6409908..00000000 --- a/STACK_QUALITY_AND_FLOW_ANALYSIS.md +++ /dev/null @@ -1,540 +0,0 @@ -# Stack Quality & Flow Analysis Report - -## Executive Summary - -This document provides a comprehensive analysis of the codebase quality, architecture patterns, and identifies critical issues in the notification and widget update flows. - -**Overall Assessment**: ⚠️ **Moderate Quality** - Good foundation with several critical issues that need attention. - ---- - -## 🔴 Critical Issues - -### 1. **Memory Leak: Multiple Polling Intervals** - -**Location**: `hooks/use-notifications.ts`, `components/parole.tsx`, `components/calendar/calendar-widget.tsx` - -**Problem**: -- `useNotifications` hook creates polling intervals that may not be properly cleaned up -- Multiple components using the hook can create duplicate intervals -- `startPolling()` returns a cleanup function but it's not properly used in the useEffect - -**Code Issue**: -```typescript -// Line 226 in use-notifications.ts -return () => stopPolling(); // This return is inside startPolling, not useEffect! -``` - -**Impact**: Memory leaks, excessive API calls, degraded performance - -**Fix Required**: -```typescript -useEffect(() => { - isMountedRef.current = true; - - if (status === 'authenticated' && session?.user) { - fetchNotificationCount(true); - fetchNotifications(); - startPolling(); - } - - return () => { - isMountedRef.current = false; - stopPolling(); // ✅ Correct placement - }; -}, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling, stopPolling]); -``` - ---- - -### 2. **Race Condition: Notification Badge Double Fetching** - -**Location**: `components/notification-badge.tsx` - -**Problem**: -- Multiple `useEffect` hooks trigger `manualFetch()` simultaneously -- Lines 65-70, 82-87, and 92-99 all trigger fetches -- No debouncing or request deduplication - -**Code Issue**: -```typescript -// Line 65-70: Fetch on dropdown open -useEffect(() => { - if (isOpen && status === 'authenticated') { - manualFetch(); - } -}, [isOpen, status]); - -// Line 82-87: Fetch on mount -useEffect(() => { - if (status === 'authenticated') { - manualFetch(); - } -}, [status]); - -// Line 92-99: Fetch on handleOpenChange -const handleOpenChange = (open: boolean) => { - setIsOpen(open); - if (open && status === 'authenticated') { - manualFetch(); // Duplicate fetch! - } -}; -``` - -**Impact**: Unnecessary API calls, potential race conditions, poor UX - -**Fix Required**: Consolidate fetch logic, add request deduplication - ---- - -### 3. **Redis KEYS Command Performance Issue** - -**Location**: `lib/services/notifications/notification-service.ts` (line 293) - -**Problem**: -- Using `redis.keys()` which is O(N) and blocks Redis -- Can cause performance degradation in production - -**Code Issue**: -```typescript -// Line 293 - BAD -const listKeys = await redis.keys(listKeysPattern); -if (listKeys.length > 0) { - await redis.del(...listKeys); -} -``` - -**Impact**: Redis blocking, slow response times, potential timeouts - -**Fix Required**: Use `SCAN` instead of `KEYS`: -```typescript -// GOOD - Use SCAN -let cursor = '0'; -do { - const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', listKeysPattern, 'COUNT', 100); - cursor = nextCursor; - if (keys.length > 0) { - await redis.del(...keys); - } -} while (cursor !== '0'); -``` - ---- - -### 4. **Infinite Loop Risk: useEffect Dependencies** - -**Location**: `hooks/use-notifications.ts` (line 255) - -**Problem**: -- `useEffect` includes functions in dependencies that are recreated on every render -- `fetchNotificationCount`, `fetchNotifications`, `startPolling`, `stopPolling` are in deps -- These functions depend on `session?.user` which changes, causing re-renders - -**Code Issue**: -```typescript -useEffect(() => { - // ... -}, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling, stopPolling]); -// ❌ Functions are recreated, causing infinite loops -``` - -**Impact**: Infinite re-renders, excessive API calls, browser freezing - -**Fix Required**: Remove function dependencies or use `useCallback` properly - ---- - -### 5. **Background Refresh Memory Leak** - -**Location**: `lib/services/notifications/notification-service.ts` (line 326) - -**Problem**: -- `setTimeout` in `scheduleBackgroundRefresh` creates closures that may not be cleaned up -- No way to cancel pending background refreshes -- Can accumulate in serverless environments - -**Code Issue**: -```typescript -setTimeout(async () => { - // This closure holds references and may not be garbage collected - await this.getNotificationCount(userId); - await this.getNotifications(userId, 1, 20); -}, 0); -``` - -**Impact**: Memory leaks, especially in serverless/edge environments - -**Fix Required**: Use proper cleanup mechanism or job queue - ---- - -## ⚠️ High Priority Issues - -### 6. **Widget Update Race Conditions** - -**Location**: Multiple widget components - -**Problem**: -- Widgets don't coordinate updates -- Multiple widgets can trigger simultaneous API calls -- No request deduplication - -**Affected Widgets**: -- `components/calendar.tsx` - Auto-refresh every 5 minutes -- `components/parole.tsx` - Auto-polling every 30 seconds -- `components/news.tsx` - Manual refresh only -- `components/flow.tsx` - Manual refresh only -- `components/email.tsx` - Manual refresh only - -**Impact**: Unnecessary load on backend, potential rate limiting - -**Fix Required**: Implement request deduplication layer or use React Query/SWR - ---- - -### 7. **Redis Connection Singleton Issues** - -**Location**: `lib/redis.ts` - -**Problem**: -- Singleton pattern but no proper connection pooling -- In serverless environments, connections may not be reused -- No connection health monitoring -- Race condition in `getRedisClient()` when `isConnecting` is true - -**Code Issue**: -```typescript -if (isConnecting) { - if (redisClient) return redisClient; - // ⚠️ What if redisClient is null but isConnecting is true? - console.warn('Redis connection in progress, creating temporary client'); -} -``` - -**Impact**: Connection leaks, connection pool exhaustion, degraded performance - -**Fix Required**: Implement proper connection pool or use Redis connection manager - ---- - -### 8. **Error Handling Gaps** - -**Location**: Multiple files - -**Problems**: -- Errors are logged but not always handled gracefully -- No retry logic for transient failures -- No circuit breaker pattern -- Widgets show errors but don't recover automatically - -**Examples**: -- `components/notification-badge.tsx` - Shows error but no auto-retry -- `lib/services/notifications/notification-service.ts` - Errors return empty arrays silently -- Widget components - Errors stop updates, no recovery - -**Impact**: Poor UX, silent failures, degraded functionality - ---- - -### 9. **Cache Invalidation Issues** - -**Location**: `lib/services/notifications/notification-service.ts` - -**Problem**: -- Cache invalidation uses `KEYS` command (blocking) -- No partial cache invalidation -- Background refresh may not invalidate properly -- Race condition: cache can be invalidated while being refreshed - -**Impact**: Stale data, inconsistent state - ---- - -### 10. **Excessive Logging** - -**Location**: Throughout codebase - -**Problem**: -- Console.log statements everywhere -- No log levels -- Production code has debug logs -- Performance impact from string concatenation - -**Impact**: Performance degradation, log storage costs, security concerns - -**Fix Required**: Use proper logging library with levels (e.g., Winston, Pino) - ---- - -## 📊 Architecture Quality Assessment - -### Strengths ✅ - -1. **Adapter Pattern**: Well-implemented notification adapter pattern -2. **Separation of Concerns**: Clear separation between services, hooks, and components -3. **Type Safety**: Good TypeScript usage -4. **Caching Strategy**: Redis caching implemented -5. **Error Boundaries**: Some error handling present - -### Weaknesses ❌ - -1. **No State Management**: Using local state instead of global state management -2. **No Request Deduplication**: Multiple components can trigger same API calls -3. **No Request Cancellation**: No way to cancel in-flight requests -4. **No Optimistic Updates**: UI doesn't update optimistically -5. **No Offline Support**: No handling for offline scenarios -6. **No Request Queue**: No queuing mechanism for API calls - ---- - -## 🔄 Flow Analysis - -### Notification Flow Issues - -#### Flow Diagram (Current - Problematic): -``` -User Action / Polling - ↓ -useNotifications Hook (multiple instances) - ↓ -Multiple API Calls (no deduplication) - ↓ -NotificationService (Redis cache check) - ↓ -Adapter Calls (parallel, but no error aggregation) - ↓ -Response (may be stale due to race conditions) -``` - -#### Issues: -1. **Multiple Hook Instances**: `NotificationBadge` and potentially other components use `useNotifications`, creating multiple polling intervals -2. **No Request Deduplication**: Same request can be made multiple times simultaneously -3. **Cache Race Conditions**: Background refresh can conflict with user requests -4. **No Request Cancellation**: Old requests aren't cancelled when new ones start - -### Widget Update Flow Issues - -#### Flow Diagram (Current - Problematic): -``` -Component Mount - ↓ -useEffect triggers fetch - ↓ -API Call (no coordination with other widgets) - ↓ -State Update (may cause unnecessary re-renders) - ↓ -Auto-refresh interval (no cleanup guarantee) -``` - -#### Issues: -1. **No Coordination**: Widgets don't know about each other's updates -2. **Duplicate Requests**: Same data fetched multiple times -3. **Cleanup Issues**: Intervals may not be cleaned up properly -4. **No Stale-While-Revalidate**: No background updates - ---- - -## 🎯 Recommendations - -### Immediate Actions (Critical) - -1. **Fix Memory Leaks** - - Fix `useNotifications` cleanup - - Ensure all intervals are cleared - - Add cleanup in all widget components - -2. **Fix Race Conditions** - - Implement request deduplication - - Fix notification badge double fetching - - Add request cancellation - -3. **Fix Redis Performance** - - Replace `KEYS` with `SCAN` - - Implement proper connection pooling - - Add connection health checks - -### Short-term Improvements (High Priority) - -1. **Implement Request Management** - - Use React Query or SWR for request deduplication - - Implement request cancellation - - Add request queuing - -2. **Improve Error Handling** - - Add retry logic with exponential backoff - - Implement circuit breaker pattern - - Add error boundaries - -3. **Optimize Caching** - - Implement stale-while-revalidate pattern - - Add cache versioning - - Improve cache invalidation strategy - -### Long-term Improvements (Medium Priority) - -1. **State Management** - - Consider Zustand or Redux for global state - - Centralize notification state - - Implement optimistic updates - -2. **Monitoring & Observability** - - Add proper logging (Winston/Pino) - - Implement metrics collection - - Add performance monitoring - -3. **Testing** - - Add unit tests for hooks - - Add integration tests for flows - - Add E2E tests for critical paths - ---- - -## 📈 Performance Metrics (Estimated) - -### Current Performance Issues: - -1. **API Calls**: - - Estimated 2-3x more calls than necessary due to race conditions - - No request deduplication - -2. **Memory Usage**: - - Potential memory leaks from uncleaned intervals - - Closures holding references - -3. **Redis Performance**: - - `KEYS` command can block for seconds with many keys - - No connection pooling - -4. **Bundle Size**: - - Excessive logging increases bundle size - - No code splitting for widgets - ---- - -## 🔍 Code Quality Metrics - -### Code Smells Found: - -1. **Long Functions**: Some functions exceed 50 lines -2. **High Cyclomatic Complexity**: `useNotifications` hook has high complexity -3. **Duplicate Code**: Similar fetch patterns across widgets -4. **Magic Numbers**: Hardcoded intervals (300000, 60000, etc.) -5. **Inconsistent Error Handling**: Different error handling patterns - -### Technical Debt: - -- **Estimated**: Medium-High -- **Areas**: - - Memory management - - Request management - - Error handling - - Caching strategy - - Logging infrastructure - ---- - -## 🛠️ Specific Code Fixes Needed - -### Fix 1: useNotifications Hook Cleanup - -```typescript -// BEFORE (Current - Problematic) -useEffect(() => { - isMountedRef.current = true; - - if (status === 'authenticated' && session?.user) { - fetchNotificationCount(true); - fetchNotifications(); - startPolling(); - } - - return () => { - isMountedRef.current = false; - stopPolling(); - }; -}, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling, stopPolling]); - -// AFTER (Fixed) -useEffect(() => { - if (status !== 'authenticated' || !session?.user) return; - - isMountedRef.current = true; - - // Initial fetch - fetchNotificationCount(true); - fetchNotifications(); - - // Start polling - const intervalId = setInterval(() => { - if (isMountedRef.current) { - debouncedFetchCount(); - } - }, POLLING_INTERVAL); - - // Cleanup - return () => { - isMountedRef.current = false; - clearInterval(intervalId); - }; -}, [status, session?.user?.id]); // Only depend on primitive values -``` - -### Fix 2: Notification Badge Deduplication - -```typescript -// Add request deduplication -const fetchInProgressRef = useRef(false); - -const manualFetch = async () => { - if (fetchInProgressRef.current) { - console.log('[NOTIFICATION_BADGE] Fetch already in progress, skipping'); - return; - } - - fetchInProgressRef.current = true; - try { - await fetchNotifications(1, 10); - } finally { - fetchInProgressRef.current = false; - } -}; -``` - -### Fix 3: Redis SCAN Instead of KEYS - -```typescript -// BEFORE -const listKeys = await redis.keys(listKeysPattern); - -// AFTER -const listKeys: string[] = []; -let cursor = '0'; -do { - const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', listKeysPattern, 'COUNT', 100); - cursor = nextCursor; - listKeys.push(...keys); -} while (cursor !== '0'); -``` - ---- - -## 📝 Conclusion - -The codebase has a solid foundation with good architectural patterns (adapter pattern, separation of concerns), but suffers from several critical issues: - -1. **Memory leaks** from improper cleanup -2. **Race conditions** from lack of request coordination -3. **Performance issues** from blocking Redis operations -4. **Error handling gaps** that degrade UX - -**Priority**: Fix critical issues immediately, then implement improvements incrementally. - -**Estimated Effort**: -- Critical fixes: 2-3 days -- High priority improvements: 1-2 weeks -- Long-term improvements: 1-2 months - ---- - -*Generated: Comprehensive codebase analysis* diff --git a/UNIFIED_REFRESH_SUMMARY.md b/UNIFIED_REFRESH_SUMMARY.md deleted file mode 100644 index c36a596d..00000000 --- a/UNIFIED_REFRESH_SUMMARY.md +++ /dev/null @@ -1,302 +0,0 @@ -# Unified Refresh System - Implementation Summary - -## ✅ What Has Been Created - -### Core Infrastructure Files - -1. **`lib/constants/refresh-intervals.ts`** - - Standardized refresh intervals for all resources - - Helper functions for interval management - - All intervals harmonized and documented - -2. **`lib/utils/request-deduplication.ts`** - - Request deduplication utility - - Prevents duplicate API calls within 5 seconds - - Automatic cleanup of stale requests - -3. **`lib/services/refresh-manager.ts`** - - Centralized refresh management - - Handles all refresh intervals - - Provides pause/resume functionality - - Prevents duplicate refreshes - -4. **`hooks/use-unified-refresh.ts`** - - React hook for easy integration - - Automatic registration/cleanup - - Manual refresh support - -### Documentation Files - -1. **`IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md`** - - Complete architecture overview - - Detailed implementation guide - - Code examples for all widgets - -2. **`IMPLEMENTATION_CHECKLIST.md`** - - Step-by-step checklist - - Daily progress tracking - - Success criteria - ---- - -## 🎯 Next Steps - -### Immediate Actions (Start Here) - -#### 1. Fix Critical Memory Leaks (30 minutes) - -**File**: `lib/services/notifications/notification-service.ts` - -Replace `redis.keys()` with `redis.scan()`: - -```typescript -// Line 293 - BEFORE -const listKeys = await redis.keys(listKeysPattern); - -// AFTER -const listKeys: string[] = []; -let cursor = '0'; -do { - const [nextCursor, keys] = await redis.scan( - cursor, - 'MATCH', - listKeysPattern, - 'COUNT', - 100 - ); - cursor = nextCursor; - if (keys.length > 0) { - listKeys.push(...keys); - } -} while (cursor !== '0'); -``` - ---- - -#### 2. Test Core Infrastructure (1 hour) - -Create a test file to verify everything works: - -**File**: `lib/services/__tests__/refresh-manager.test.ts` (optional) - -Or test manually: -1. Import refresh manager in a component -2. Register a test resource -3. Verify it refreshes at correct interval -4. Verify cleanup on unmount - ---- - -#### 3. Refactor Notifications (2-3 hours) - -**File**: `hooks/use-notifications.ts` - -Key changes: -- Remove manual polling logic -- Use `useUnifiedRefresh` hook -- Add `requestDeduplicator` for API calls -- Fix useEffect dependencies - -See `IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md` Section 3.1 for full code. - ---- - -#### 4. Refactor Notification Badge (1 hour) - -**File**: `components/notification-badge.tsx` - -Key changes: -- Remove duplicate `useEffect` hooks -- Use hook's `refresh` function for manual refresh -- Remove manual fetch logic - ---- - -#### 5. Refactor Navigation Bar Time (30 minutes) - -**File**: `components/main-nav.tsx` + `components/main-nav-time.tsx` (new) - -Key changes: -- Extract time display to separate component -- Use `useUnifiedRefresh` hook (1 second interval) -- Fix static time issue - -See `IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md` Section 3.7 for full code. - ---- - -#### 6. Refactor Widgets (1 hour each) - -Start with high-frequency widgets: -1. **Parole** (`components/parole.tsx`) - 30s interval -2. **Calendar** (`components/calendar.tsx`) - 5min interval -3. **News** (`components/news.tsx`) - 10min interval -4. **Email** (`components/email.tsx`) - 1min interval -5. **Duties** (`components/flow.tsx`) - 2min interval - -See `IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md` Section 3.2 for example code. - ---- - -## 📊 Expected Results - -### Before Implementation: -- ❌ 120-150 API calls/minute -- ❌ Memory leaks from uncleaned intervals -- ❌ Duplicate requests -- ❌ No coordination between widgets - -### After Implementation: -- ✅ 40-50 API calls/minute (60-70% reduction) -- ✅ No memory leaks -- ✅ Request deduplication working -- ✅ Centralized refresh coordination - ---- - -## 🔍 Testing Checklist - -After each phase, verify: - -- [ ] No console errors -- [ ] Widgets refresh at correct intervals -- [ ] Manual refresh buttons work -- [ ] No duplicate API calls (check Network tab) -- [ ] No memory leaks (check Memory tab) -- [ ] Cleanup on component unmount -- [ ] Multiple tabs don't cause issues - ---- - -## 🚨 Important Notes - -### Backward Compatibility - -All new code is designed to be: -- ✅ Non-breaking (old code still works) -- ✅ Gradual migration (one widget at a time) -- ✅ Easy rollback (keep old implementations) - -### Migration Strategy - -1. **Phase 1**: Core infrastructure (DONE ✅) -2. **Phase 2**: Fix critical issues -3. **Phase 3**: Migrate notifications -4. **Phase 4**: Migrate widgets one by one -5. **Phase 5**: Remove old code - -### Feature Flags (Optional) - -If you want to toggle the new system: - -```typescript -// In refresh manager -const USE_UNIFIED_REFRESH = process.env.NEXT_PUBLIC_USE_UNIFIED_REFRESH !== 'false'; - -if (USE_UNIFIED_REFRESH) { - // Use new system -} else { - // Use old system -} -``` - ---- - -## 📈 Performance Monitoring - -### Metrics to Track - -1. **API Call Count** - - Before: ~120-150/min - - Target: ~40-50/min - - Monitor in Network tab - -2. **Memory Usage** - - Before: Growing over time - - Target: Stable - - Monitor in Memory tab - -3. **Refresh Accuracy** - - Verify intervals are correct - - Check last refresh times - - Monitor refresh manager status - -### Debug Tools - -```typescript -// Get refresh manager status -const status = refreshManager.getStatus(); -console.log('Refresh Manager Status:', status); - -// Get pending requests -const pendingCount = requestDeduplicator.getPendingCount(); -console.log('Pending Requests:', pendingCount); -``` - ---- - -## 🎓 Learning Resources - -### Key Concepts - -1. **Singleton Pattern**: Refresh manager uses singleton -2. **Request Deduplication**: Prevents duplicate calls -3. **React Hooks**: Proper cleanup with useEffect -4. **Memory Management**: Clearing intervals and refs - -### Code Patterns - -- **useRef for callbacks**: Prevents dependency issues -- **Map for tracking**: Efficient resource management -- **Promise tracking**: Prevents duplicate requests - ---- - -## 🐛 Troubleshooting - -### Issue: Widgets not refreshing - -**Check**: -1. Is refresh manager started? (`refreshManager.start()`) -2. Is resource registered? (`refreshManager.getStatus()`) -3. Is user authenticated? (`status === 'authenticated'`) - -### Issue: Duplicate API calls - -**Check**: -1. Is request deduplication working? (`requestDeduplicator.getPendingCount()`) -2. Are multiple components using the same resource? -3. Is TTL too short? - -### Issue: Memory leaks - -**Check**: -1. Are intervals cleaned up? (check cleanup functions) -2. Are refs cleared? (`isMountedRef.current = false`) -3. Are pending requests cleared? (check cleanup) - ---- - -## 📝 Next Session Goals - -1. ✅ Core infrastructure created -2. ⏭️ Fix Redis KEYS → SCAN -3. ⏭️ Refactor notifications hook -4. ⏭️ Refactor notification badge -5. ⏭️ Refactor first widget (Parole) - ---- - -## 🎉 Success! - -Once all widgets are migrated: - -- ✅ Unified refresh system -- ✅ 60%+ reduction in API calls -- ✅ No memory leaks -- ✅ Better user experience -- ✅ Easier maintenance - ---- - -*Last Updated: Implementation Summary v1.0* diff --git a/app/api/missions/test-n8n-config/route.ts b/app/api/missions/test-n8n-config/route.ts new file mode 100644 index 00000000..5896e2d9 --- /dev/null +++ b/app/api/missions/test-n8n-config/route.ts @@ -0,0 +1,139 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from "@/app/api/auth/options"; +import { logger } from '@/lib/logger'; + +/** + * GET /api/missions/test-n8n-config + * + * Endpoint de test pour vérifier la configuration N8N + * Permet de diagnostiquer les problèmes de connexion entre Next.js et N8N + * + * Authentification: Requise (session utilisateur) + */ +export async function GET(request: Request) { + try { + // Vérifier l'authentification + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Récupérer les variables d'environnement + const n8nApiKey = process.env.N8N_API_KEY; + const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-created'; + const n8nRollbackWebhookUrl = process.env.N8N_ROLLBACK_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-rollback'; + const missionApiUrl = process.env.NEXT_PUBLIC_API_URL || 'https://api.slm-lab.net/api'; + const n8nDeleteWebhookUrl = process.env.N8N_DELETE_WEBHOOK_URL; + + // Construire la réponse + const config = { + // Variables d'environnement + environment: { + hasN8NApiKey: !!n8nApiKey, + n8nApiKeyLength: n8nApiKey?.length || 0, + n8nApiKeyPrefix: n8nApiKey ? `${n8nApiKey.substring(0, 4)}...` : 'none', + n8nWebhookUrl, + n8nRollbackWebhookUrl, + n8nDeleteWebhookUrl: n8nDeleteWebhookUrl || 'not configured', + missionApiUrl, + }, + + // URLs construites + urls: { + webhookUrl: n8nWebhookUrl, + callbackUrl: `${missionApiUrl}/api/missions/mission-created`, + rollbackUrl: n8nRollbackWebhookUrl, + deleteUrl: n8nDeleteWebhookUrl || 'not configured', + }, + + // Statut de configuration + status: { + configured: !!n8nApiKey && !!missionApiUrl, + missingApiKey: !n8nApiKey, + missingApiUrl: !missionApiUrl, + ready: !!n8nApiKey && !!missionApiUrl, + }, + + // Recommandations + recommendations: [] as string[], + }; + + // Ajouter des recommandations basées sur la configuration + if (!n8nApiKey) { + config.recommendations.push('❌ N8N_API_KEY n\'est pas défini. Ajoutez-le à vos variables d\'environnement.'); + } else { + config.recommendations.push('✅ N8N_API_KEY est configuré'); + } + + if (!missionApiUrl) { + config.recommendations.push('⚠️ NEXT_PUBLIC_API_URL n\'est pas défini. Utilisation de la valeur par défaut.'); + } else { + config.recommendations.push('✅ NEXT_PUBLIC_API_URL est configuré'); + } + + if (n8nApiKey && n8nApiKey.length < 10) { + config.recommendations.push('⚠️ N8N_API_KEY semble trop court. Vérifiez qu\'il est correct.'); + } + + // Tester la connectivité au webhook N8N (optionnel, peut être lent) + const testWebhook = request.headers.get('x-test-webhook') === 'true'; + if (testWebhook) { + try { + logger.debug('Testing N8N webhook connectivity', { url: n8nWebhookUrl }); + const testResponse = await fetch(n8nWebhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': n8nApiKey || '', + }, + body: JSON.stringify({ test: true }), + signal: AbortSignal.timeout(5000), // 5 secondes timeout + }); + + config.urls.webhookTest = { + status: testResponse.status, + statusText: testResponse.statusText, + reachable: testResponse.status !== 0, + note: testResponse.status === 404 + ? 'Webhook non enregistré (workflow inactif?)' + : testResponse.status === 200 || testResponse.status === 400 || testResponse.status === 500 + ? 'Webhook actif (peut échouer avec des données de test)' + : 'Réponse inattendue', + }; + } catch (error) { + config.urls.webhookTest = { + error: error instanceof Error ? error.message : 'Unknown error', + reachable: false, + note: 'Impossible de joindre le webhook N8N', + }; + } + } else { + config.urls.webhookTest = { + note: 'Ajoutez le header "x-test-webhook: true" pour tester la connectivité', + }; + } + + return NextResponse.json({ + success: true, + timestamp: new Date().toISOString(), + ...config, + }); + } catch (error) { + logger.error('Error in test-n8n-config endpoint', { + error: error instanceof Error ? error.message : String(error) + }); + return NextResponse.json( + { + success: false, + error: 'Failed to check N8N configuration', + details: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} + diff --git a/log b/log deleted file mode 100644 index 8b137891..00000000 --- a/log +++ /dev/null @@ -1 +0,0 @@ -