diff --git a/prisma/migrations/20250115120000_add_user_id_to_preferences/migration.sql b/prisma/migrations/20250115120000_add_user_id_to_preferences/migration.sql new file mode 100644 index 0000000..353ed36 --- /dev/null +++ b/prisma/migrations/20250115120000_add_user_id_to_preferences/migration.sql @@ -0,0 +1,23 @@ +-- Migration pour ajouter userId aux UserPreferences +-- et migrer les données existantes vers le premier utilisateur + +-- 1. Ajouter la colonne userId (nullable temporairement) +ALTER TABLE "user_preferences" ADD COLUMN "userId" TEXT; + +-- 2. Créer un index unique sur userId +CREATE UNIQUE INDEX "user_preferences_userId_key" ON "user_preferences"("userId"); + +-- 3. Migrer les données existantes vers le premier utilisateur +-- (on suppose qu'il y a au moins un utilisateur dans la table users) +UPDATE "user_preferences" +SET "userId" = (SELECT id FROM "users" LIMIT 1) +WHERE "userId" IS NULL; + +-- 4. Rendre la colonne userId non-nullable +-- Note: SQLite ne supporte pas ALTER COLUMN, donc on doit recréer la table +-- Mais comme on a déjà des données, on va juste s'assurer que toutes les entrées ont un userId +-- En production, on devrait faire une migration plus complexe + +-- 5. Ajouter la contrainte de clé étrangère +-- SQLite ne supporte pas les contraintes de clé étrangère dans ALTER TABLE +-- La contrainte sera gérée par Prisma au niveau applicatif diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 679d34a..f9428f5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,7 @@ model User { password String // Hashé avec bcrypt createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + preferences UserPreferences? @@map("users") } @@ -101,6 +102,7 @@ model DailyCheckbox { model UserPreferences { id String @id @default(cuid()) + userId String @unique kanbanFilters Json? viewPreferences Json? columnVisibility Json? @@ -112,6 +114,7 @@ model UserPreferences { tfsSyncInterval String @default("daily") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("user_preferences") } diff --git a/scripts/test-jira-fields.ts b/scripts/test-jira-fields.ts index 11be27d..cad9ca5 100644 --- a/scripts/test-jira-fields.ts +++ b/scripts/test-jira-fields.ts @@ -13,7 +13,7 @@ async function testJiraFields() { try { // Récupérer la config Jira - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig('default'); if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { console.log('❌ Configuration Jira manquante'); diff --git a/scripts/test-story-points.ts b/scripts/test-story-points.ts index 28ec586..b06388c 100644 --- a/scripts/test-story-points.ts +++ b/scripts/test-story-points.ts @@ -13,7 +13,7 @@ async function testStoryPoints() { try { // Récupérer la config Jira - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig('default'); if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { console.log('❌ Configuration Jira manquante'); diff --git a/src/actions/jira-analytics.ts b/src/actions/jira-analytics.ts index c88b7dc..821b5e2 100644 --- a/src/actions/jira-analytics.ts +++ b/src/actions/jira-analytics.ts @@ -3,6 +3,8 @@ import { JiraAnalyticsService } from '@/services/integrations/jira/analytics'; import { userPreferencesService } from '@/services/core/user-preferences'; import { JiraAnalytics } from '@/lib/types'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; export type JiraAnalyticsResult = { success: boolean; @@ -15,8 +17,13 @@ export type JiraAnalyticsResult = { */ export async function getJiraAnalytics(forceRefresh = false): Promise { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + // Récupérer la config Jira depuis la base de données - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { return { diff --git a/src/actions/jira-anomalies.ts b/src/actions/jira-anomalies.ts index 13f23d2..a357a5e 100644 --- a/src/actions/jira-anomalies.ts +++ b/src/actions/jira-anomalies.ts @@ -3,6 +3,8 @@ import { jiraAnomalyDetection, JiraAnomaly, AnomalyDetectionConfig } from '@/services/integrations/jira/anomaly-detection'; import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/integrations/jira/analytics'; import { userPreferencesService } from '@/services/core/user-preferences'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; export interface AnomalyDetectionResult { success: boolean; @@ -15,8 +17,13 @@ export interface AnomalyDetectionResult { */ export async function detectJiraAnomalies(forceRefresh = false): Promise { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + // Récupérer la config Jira - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) { return { diff --git a/src/actions/jira-filters.ts b/src/actions/jira-filters.ts index 3840e42..5102b4f 100644 --- a/src/actions/jira-filters.ts +++ b/src/actions/jira-filters.ts @@ -4,6 +4,8 @@ import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/integratio import { JiraAdvancedFiltersService } from '@/services/integrations/jira/advanced-filters'; import { userPreferencesService } from '@/services/core/user-preferences'; import { AvailableFilters, JiraAnalyticsFilters, JiraAnalytics } from '@/lib/types'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; export interface FiltersResult { success: boolean; @@ -22,8 +24,13 @@ export interface FilteredAnalyticsResult { */ export async function getAvailableJiraFilters(): Promise { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + // Récupérer la config Jira - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) { return { @@ -63,8 +70,13 @@ export async function getAvailableJiraFilters(): Promise { */ export async function getFilteredJiraAnalytics(filters: Partial): Promise { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + // Récupérer la config Jira - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) { return { diff --git a/src/actions/jira-sprint-details.ts b/src/actions/jira-sprint-details.ts index aa595f6..a85869f 100644 --- a/src/actions/jira-sprint-details.ts +++ b/src/actions/jira-sprint-details.ts @@ -5,6 +5,8 @@ import { userPreferencesService } from '@/services/core/user-preferences'; import { SprintDetails } from '@/components/jira/SprintDetailModal'; import { JiraTask, AssigneeDistribution, StatusDistribution, SprintVelocity } from '@/lib/types'; import { parseDate } from '@/lib/date-utils'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; export interface SprintDetailsResult { success: boolean; @@ -17,8 +19,13 @@ export interface SprintDetailsResult { */ export async function getSprintDetails(sprintName: string): Promise { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + // Récupérer la config Jira - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) { return { diff --git a/src/actions/preferences.ts b/src/actions/preferences.ts index fc1e51e..425ad37 100644 --- a/src/actions/preferences.ts +++ b/src/actions/preferences.ts @@ -4,6 +4,8 @@ import { userPreferencesService } from '@/services/core/user-preferences'; import { KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types'; import { Theme } from '@/lib/theme-config'; import { revalidatePath } from 'next/cache'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; /** * Met à jour les préférences de vue @@ -13,7 +15,12 @@ export async function updateViewPreferences(updates: Partial): error?: string; }> { try { - await userPreferencesService.updateViewPreferences(updates); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + await userPreferencesService.updateViewPreferences(session.user.id, updates); revalidatePath('/'); return { success: true }; } catch (error) { @@ -33,7 +40,12 @@ export async function updateKanbanFilters(updates: Partial): Prom error?: string; }> { try { - await userPreferencesService.updateKanbanFilters(updates); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + await userPreferencesService.updateKanbanFilters(session.user.id, updates); revalidatePath('/kanban'); return { success: true }; } catch (error) { @@ -53,13 +65,18 @@ export async function updateColumnVisibility(updates: Partial) error?: string; }> { try { - const preferences = await userPreferencesService.getAllPreferences(); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + const preferences = await userPreferencesService.getAllPreferences(session.user.id); const newColumnVisibility: ColumnVisibility = { ...preferences.columnVisibility, ...updates }; - await userPreferencesService.saveColumnVisibility(newColumnVisibility); + await userPreferencesService.saveColumnVisibility(session.user.id, newColumnVisibility); revalidatePath('/kanban'); return { success: true }; } catch (error) { @@ -79,10 +96,15 @@ export async function toggleObjectivesVisibility(): Promise<{ error?: string; }> { try { - const preferences = await userPreferencesService.getAllPreferences(); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + const preferences = await userPreferencesService.getAllPreferences(session.user.id); const showObjectives = !preferences.viewPreferences.showObjectives; - await userPreferencesService.updateViewPreferences({ showObjectives }); + await userPreferencesService.updateViewPreferences(session.user.id, { showObjectives }); revalidatePath('/'); return { success: true }; } catch (error) { @@ -102,10 +124,15 @@ export async function toggleObjectivesCollapse(): Promise<{ error?: string; }> { try { - const preferences = await userPreferencesService.getAllPreferences(); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + const preferences = await userPreferencesService.getAllPreferences(session.user.id); const collapseObjectives = !preferences.viewPreferences.collapseObjectives; - await userPreferencesService.updateViewPreferences({ collapseObjectives }); + await userPreferencesService.updateViewPreferences(session.user.id, { collapseObjectives }); revalidatePath('/'); return { success: true }; } catch (error) { @@ -125,7 +152,12 @@ export async function setTheme(theme: Theme): Promise<{ error?: string; }> { try { - await userPreferencesService.updateViewPreferences({ theme }); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + await userPreferencesService.updateViewPreferences(session.user.id, { theme }); revalidatePath('/'); return { success: true }; } catch (error) { @@ -145,10 +177,15 @@ export async function toggleTheme(): Promise<{ error?: string; }> { try { - const preferences = await userPreferencesService.getAllPreferences(); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + const preferences = await userPreferencesService.getAllPreferences(session.user.id); const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark'; - await userPreferencesService.updateViewPreferences({ theme: newTheme }); + await userPreferencesService.updateViewPreferences(session.user.id, { theme: newTheme }); revalidatePath('/'); return { success: true }; } catch (error) { @@ -168,13 +205,18 @@ export async function toggleFontSize(): Promise<{ error?: string; }> { try { - const preferences = await userPreferencesService.getAllPreferences(); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + const preferences = await userPreferencesService.getAllPreferences(session.user.id); const fontSizes: ('small' | 'medium' | 'large')[] = ['small', 'medium', 'large']; const currentIndex = fontSizes.indexOf(preferences.viewPreferences.fontSize); const nextIndex = (currentIndex + 1) % fontSizes.length; const newFontSize = fontSizes[nextIndex]; - await userPreferencesService.updateViewPreferences({ fontSize: newFontSize }); + await userPreferencesService.updateViewPreferences(session.user.id, { fontSize: newFontSize }); revalidatePath('/'); return { success: true }; } catch (error) { @@ -194,7 +236,12 @@ export async function toggleColumnVisibility(status: TaskStatus): Promise<{ error?: string; }> { try { - const preferences = await userPreferencesService.getAllPreferences(); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + const preferences = await userPreferencesService.getAllPreferences(session.user.id); const hiddenStatuses = new Set(preferences.columnVisibility.hiddenStatuses); if (hiddenStatuses.has(status)) { @@ -203,7 +250,7 @@ export async function toggleColumnVisibility(status: TaskStatus): Promise<{ hiddenStatuses.add(status); } - await userPreferencesService.saveColumnVisibility({ + await userPreferencesService.saveColumnVisibility(session.user.id, { hiddenStatuses: Array.from(hiddenStatuses) }); diff --git a/src/actions/tfs.ts b/src/actions/tfs.ts index e1e3677..e4567f7 100644 --- a/src/actions/tfs.ts +++ b/src/actions/tfs.ts @@ -3,13 +3,20 @@ import { userPreferencesService } from '@/services/core/user-preferences'; import { revalidatePath } from 'next/cache'; import { tfsService, TfsConfig } from '@/services/integrations/tfs'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; /** * Sauvegarde la configuration TFS */ export async function saveTfsConfig(config: TfsConfig) { try { - await userPreferencesService.saveTfsConfig(config); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + await userPreferencesService.saveTfsConfig(session.user.id, config); // Réinitialiser le service pour prendre en compte la nouvelle config tfsService.reset(); @@ -34,7 +41,12 @@ export async function saveTfsConfig(config: TfsConfig) { */ export async function getTfsConfig() { try { - const config = await userPreferencesService.getTfsConfig(); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + const config = await userPreferencesService.getTfsConfig(session.user.id); return { success: true, data: config }; } catch (error) { console.error('Erreur récupération config TFS:', error); @@ -64,7 +76,13 @@ export async function saveTfsSchedulerConfig( tfsSyncInterval: 'hourly' | 'daily' | 'weekly' ) { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + await userPreferencesService.saveTfsSchedulerConfig( + session.user.id, tfsAutoSync, tfsSyncInterval ); diff --git a/src/app/api/jira/sync/route.ts b/src/app/api/jira/sync/route.ts index 272673b..77d1c95 100644 --- a/src/app/api/jira/sync/route.ts +++ b/src/app/api/jira/sync/route.ts @@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'; import { createJiraService, JiraService } from '@/services/integrations/jira/jira'; import { userPreferencesService } from '@/services/core/user-preferences'; import { jiraScheduler } from '@/services/integrations/jira/scheduler'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; /** * Route POST /api/jira/sync @@ -10,6 +12,14 @@ import { jiraScheduler } from '@/services/integrations/jira/scheduler'; */ export async function POST(request: Request) { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { success: false, error: 'Non authentifié' }, + { status: 401 } + ); + } + // Vérifier s'il y a des actions spécifiques (scheduler) const body = await request.json().catch(() => ({})); const { action, ...params } = body; @@ -30,6 +40,7 @@ export async function POST(request: Request) { case 'config': await userPreferencesService.saveJiraSchedulerConfig( + session.user.id, params.jiraAutoSync, params.jiraSyncInterval ); @@ -50,7 +61,7 @@ export async function POST(request: Request) { } // Synchronisation normale (manuelle) - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); let jiraService: JiraService | null = null; @@ -124,8 +135,16 @@ export async function POST(request: Request) { */ export async function GET() { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { success: false, error: 'Non authentifié' }, + { status: 401 } + ); + } + // Essayer d'abord la config depuis la base de données - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); let jiraService: JiraService | null = null; diff --git a/src/app/api/jira/validate-project/route.ts b/src/app/api/jira/validate-project/route.ts index 93171c8..a11146f 100644 --- a/src/app/api/jira/validate-project/route.ts +++ b/src/app/api/jira/validate-project/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { createJiraService } from '@/services/integrations/jira/jira'; import { userPreferencesService } from '@/services/core/user-preferences'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; /** * POST /api/jira/validate-project @@ -8,6 +10,14 @@ import { userPreferencesService } from '@/services/core/user-preferences'; */ export async function POST(request: NextRequest) { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Non authentifié' }, + { status: 401 } + ); + } + const body = await request.json(); const { projectKey } = body; @@ -19,7 +29,7 @@ export async function POST(request: NextRequest) { } // Récupérer la config Jira depuis la base de données - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { return NextResponse.json( diff --git a/src/app/api/user-preferences/jira-config/route.ts b/src/app/api/user-preferences/jira-config/route.ts index 3d5bac7..6a641a4 100644 --- a/src/app/api/user-preferences/jira-config/route.ts +++ b/src/app/api/user-preferences/jira-config/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { userPreferencesService } from '@/services/core/user-preferences'; import { JiraConfig } from '@/lib/types'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; /** * GET /api/user-preferences/jira-config @@ -8,7 +10,15 @@ import { JiraConfig } from '@/lib/types'; */ export async function GET() { try { - const jiraConfig = await userPreferencesService.getJiraConfig(); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Non authentifié' }, + { status: 401 } + ); + } + + const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); return NextResponse.json({ jiraConfig }); } catch (error) { console.error('Erreur lors de la récupération de la config Jira:', error); @@ -25,6 +35,14 @@ export async function GET() { */ export async function PUT(request: NextRequest) { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Non authentifié' }, + { status: 401 } + ); + } + const body = await request.json(); const { baseUrl, email, apiToken, projectKey, ignoredProjects } = body; @@ -66,7 +84,7 @@ export async function PUT(request: NextRequest) { : [] }; - await userPreferencesService.saveJiraConfig(jiraConfig); + await userPreferencesService.saveJiraConfig(session.user.id, jiraConfig); return NextResponse.json({ success: true, @@ -91,6 +109,14 @@ export async function PUT(request: NextRequest) { */ export async function DELETE() { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Non authentifié' }, + { status: 401 } + ); + } + const defaultConfig: JiraConfig = { baseUrl: '', email: '', @@ -99,7 +125,7 @@ export async function DELETE() { ignoredProjects: [] }; - await userPreferencesService.saveJiraConfig(defaultConfig); + await userPreferencesService.saveJiraConfig(session.user.id, defaultConfig); return NextResponse.json({ success: true, diff --git a/src/app/api/user-preferences/route.ts b/src/app/api/user-preferences/route.ts index d805fe5..b6bed9a 100644 --- a/src/app/api/user-preferences/route.ts +++ b/src/app/api/user-preferences/route.ts @@ -1,12 +1,22 @@ import { NextRequest, NextResponse } from 'next/server'; import { userPreferencesService } from '@/services/core/user-preferences'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; /** * GET /api/user-preferences - Récupère toutes les préférences utilisateur */ export async function GET() { try { - const preferences = await userPreferencesService.getAllPreferences(); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { success: false, error: 'Non authentifié' }, + { status: 401 } + ); + } + + const preferences = await userPreferencesService.getAllPreferences(session.user.id); return NextResponse.json({ success: true, @@ -29,9 +39,17 @@ export async function GET() { */ export async function PUT(request: NextRequest) { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { success: false, error: 'Non authentifié' }, + { status: 401 } + ); + } + const preferences = await request.json(); - await userPreferencesService.saveAllPreferences(preferences); + await userPreferencesService.saveAllPreferences(session.user.id, preferences); return NextResponse.json({ success: true, diff --git a/src/app/jira-dashboard/page.tsx b/src/app/jira-dashboard/page.tsx index 64d73ce..87d18a9 100644 --- a/src/app/jira-dashboard/page.tsx +++ b/src/app/jira-dashboard/page.tsx @@ -1,13 +1,20 @@ import { userPreferencesService } from '@/services/core/user-preferences'; import { getJiraAnalytics } from '@/actions/jira-analytics'; import { JiraDashboardPageClient } from './JiraDashboardPageClient'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; // Force dynamic rendering export const dynamic = 'force-dynamic'; export default async function JiraDashboardPage() { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return
Non authentifié
; + } + // Récupérer la config Jira côté serveur - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); // Récupérer les analytics côté serveur (utilise le cache du service) let initialAnalytics = null; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b9d14e5..8048b82 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,6 +8,8 @@ import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext"; import { userPreferencesService } from "@/services/core/user-preferences"; import { KeyboardShortcuts } from "@/components/KeyboardShortcuts"; import { AuthProvider } from "../components/AuthProvider"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; const geistSans = Geist({ variable: "--font-geist-sans", @@ -29,8 +31,14 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - // Récupérer toutes les préférences côté serveur pour le SSR - const initialPreferences = await userPreferencesService.getAllPreferences(); + // Récupérer la session côté serveur pour le SSR + const session = await getServerSession(authOptions); + + // Charger les préférences seulement si l'utilisateur est connecté + // Sinon, les préférences par défaut seront chargées côté client + const initialPreferences = session?.user?.id + ? await userPreferencesService.getAllPreferences(session.user.id) + : undefined; return ( @@ -39,12 +47,12 @@ export default async function RootLayout({ > - + {children} diff --git a/src/app/settings/integrations/page.tsx b/src/app/settings/integrations/page.tsx index da63ddb..837fb7a 100644 --- a/src/app/settings/integrations/page.tsx +++ b/src/app/settings/integrations/page.tsx @@ -1,15 +1,22 @@ import { userPreferencesService } from '@/services/core/user-preferences'; import { IntegrationsSettingsPageClient } from '@/components/settings/IntegrationsSettingsPageClient'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; // Force dynamic rendering for real-time data export const dynamic = 'force-dynamic'; export default async function IntegrationsSettingsPage() { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return
Non authentifié
; + } + // Fetch data server-side // Preferences are now available via context const [jiraConfig, tfsConfig] = await Promise.all([ - userPreferencesService.getJiraConfig(), - userPreferencesService.getTfsConfig() + userPreferencesService.getJiraConfig(session.user.id), + userPreferencesService.getTfsConfig(session.user.id) ]); return ( diff --git a/src/components/settings/TfsConfigForm.tsx b/src/components/settings/TfsConfigForm.tsx index 0e6fada..bec17c1 100644 --- a/src/components/settings/TfsConfigForm.tsx +++ b/src/components/settings/TfsConfigForm.tsx @@ -34,7 +34,7 @@ export function TfsConfigForm() { try { setIsLoading(true); const result = await getTfsConfig(); - if (result.success) { + if (result.success && result.data) { setConfig(result.data); // Afficher le formulaire par défaut si TFS n'est pas configuré const isConfigured = diff --git a/src/contexts/UserPreferencesContext.tsx b/src/contexts/UserPreferencesContext.tsx index cd1b210..1b0dc1f 100644 --- a/src/contexts/UserPreferencesContext.tsx +++ b/src/contexts/UserPreferencesContext.tsx @@ -12,6 +12,7 @@ import { toggleColumnVisibility as toggleColumnVisibilityAction } from '@/actions/preferences'; import { useTheme } from './ThemeContext'; +import { useSession } from 'next-auth/react'; interface UserPreferencesContextType { preferences: UserPreferences; @@ -77,6 +78,38 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr const [preferences, setPreferences] = useState(initialPreferences || defaultPreferences); const [isPending, startTransition] = useTransition(); const { theme, toggleTheme: themeToggleTheme, setTheme: themeSetTheme } = useTheme(); + const { data: session, status } = useSession(); + + // Fonction pour charger les préférences côté client + const loadUserPreferences = useCallback(async () => { + if (status === 'loading') return; // Attendre que la session soit chargée + + try { + const response = await fetch('/api/user-preferences'); + if (response.ok) { + const result = await response.json(); + if (result.success) { + setPreferences(result.data); + // Synchroniser le thème avec le ThemeContext + if (result.data.viewPreferences.theme !== theme) { + themeSetTheme(result.data.viewPreferences.theme); + } + } + } + } catch (error) { + console.error('Erreur lors du chargement des préférences:', error); + } + }, [status, theme, themeSetTheme]); + + // Recharger les préférences quand la session change (login/logout) + useEffect(() => { + if (status === 'authenticated') { + loadUserPreferences(); + } else if (status === 'unauthenticated') { + // Reset aux préférences par défaut quand l'utilisateur se déconnecte + setPreferences(defaultPreferences); + } + }, [status, loadUserPreferences]); // Synchroniser les préférences avec le thème actuel du ThemeContext useEffect(() => { diff --git a/src/services/core/user-preferences.ts b/src/services/core/user-preferences.ts index 4649573..bbcf737 100644 --- a/src/services/core/user-preferences.ts +++ b/src/services/core/user-preferences.ts @@ -59,18 +59,16 @@ const DEFAULT_PREFERENCES: UserPreferences = { * Service pour gérer les préférences utilisateur en base de données */ class UserPreferencesService { - private readonly USER_ID = 'default'; // Pour l'instant, un seul utilisateur - /** * Récupère ou crée l'entrée user preferences (avec upsert pour éviter les doublons) */ - private async getOrCreateUserPreferences() { + private async getOrCreateUserPreferences(userId: string) { // Utiliser upsert pour éviter les conditions de course const userPrefs = await prisma.userPreferences.upsert({ - where: { id: 'default' }, // ID fixe pour l'utilisateur unique + where: { userId }, // Utiliser userId au lieu de id update: {}, // Ne rien mettre à jour si existe create: { - id: 'default', + userId, kanbanFilters: DEFAULT_PREFERENCES.kanbanFilters, viewPreferences: DEFAULT_PREFERENCES.viewPreferences, columnVisibility: DEFAULT_PREFERENCES.columnVisibility, @@ -79,7 +77,7 @@ class UserPreferencesService { }); // S'assurer que les nouveaux champs existent (migration douce) - await this.ensureJiraSchedulerFields(); + await this.ensureJiraSchedulerFields(userId); return userPrefs; } @@ -87,13 +85,13 @@ class UserPreferencesService { /** * S'assure que les champs jiraAutoSync et jiraSyncInterval existent */ - private async ensureJiraSchedulerFields(): Promise { + private async ensureJiraSchedulerFields(userId: string): Promise { try { await prisma.$executeRaw` UPDATE user_preferences SET jiraAutoSync = COALESCE(jiraAutoSync, ${DEFAULT_PREFERENCES.jiraAutoSync}), jiraSyncInterval = COALESCE(jiraSyncInterval, ${DEFAULT_PREFERENCES.jiraSyncInterval}) - WHERE id = 'default' + WHERE userId = ${userId} `; } catch (error) { // Ignorer les erreurs si les colonnes n'existent pas encore @@ -106,11 +104,11 @@ class UserPreferencesService { /** * Sauvegarde les filtres Kanban */ - async saveKanbanFilters(filters: KanbanFilters): Promise { + async saveKanbanFilters(userId: string, filters: KanbanFilters): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); await prisma.userPreferences.update({ - where: { id: userPrefs.id }, + where: { userId }, data: { kanbanFilters: filters } }); } catch (error) { @@ -122,9 +120,9 @@ class UserPreferencesService { /** * Récupère les filtres Kanban */ - async getKanbanFilters(): Promise { + async getKanbanFilters(userId: string): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); const filters = userPrefs.kanbanFilters as KanbanFilters | null; return { ...DEFAULT_PREFERENCES.kanbanFilters, ...(filters || {}) }; } catch (error) { @@ -138,11 +136,11 @@ class UserPreferencesService { /** * Sauvegarde les préférences de vue */ - async saveViewPreferences(preferences: ViewPreferences): Promise { + async saveViewPreferences(userId: string, preferences: ViewPreferences): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); await prisma.userPreferences.update({ - where: { id: userPrefs.id }, + where: { userId }, data: { viewPreferences: preferences } }); } catch (error) { @@ -157,9 +155,9 @@ class UserPreferencesService { /** * Récupère les préférences de vue */ - async getViewPreferences(): Promise { + async getViewPreferences(userId: string): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); const preferences = userPrefs.viewPreferences as ViewPreferences | null; return { ...DEFAULT_PREFERENCES.viewPreferences, ...(preferences || {}) }; } catch (error) { @@ -176,11 +174,11 @@ class UserPreferencesService { /** * Sauvegarde la visibilité des colonnes */ - async saveColumnVisibility(visibility: ColumnVisibility): Promise { + async saveColumnVisibility(userId: string, visibility: ColumnVisibility): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); await prisma.userPreferences.update({ - where: { id: userPrefs.id }, + where: { userId }, data: { columnVisibility: visibility }, }); } catch (error) { @@ -195,9 +193,9 @@ class UserPreferencesService { /** * Récupère la visibilité des colonnes */ - async getColumnVisibility(): Promise { + async getColumnVisibility(userId: string): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); const visibility = userPrefs.columnVisibility as ColumnVisibility | null; return { ...DEFAULT_PREFERENCES.columnVisibility, ...(visibility || {}) }; } catch (error) { @@ -214,9 +212,9 @@ class UserPreferencesService { /** * Récupère uniquement le thème pour le SSR (optimisé) */ - async getTheme(): Promise { + async getTheme(userId: string): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); const viewPrefs = userPrefs.viewPreferences as ViewPreferences; return viewPrefs.theme; } catch (error) { @@ -230,11 +228,11 @@ class UserPreferencesService { /** * Sauvegarde la configuration Jira */ - async saveJiraConfig(config: JiraConfig): Promise { + async saveJiraConfig(userId: string, config: JiraConfig): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); await prisma.userPreferences.update({ - where: { id: userPrefs.id }, + where: { userId }, data: { jiraConfig: config as any } // eslint-disable-line @typescript-eslint/no-explicit-any }); } catch (error) { @@ -246,9 +244,9 @@ class UserPreferencesService { /** * Récupère la configuration Jira depuis la base de données avec fallback sur les variables d'environnement */ - async getJiraConfig(): Promise { + async getJiraConfig(userId: string): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); const dbConfig = userPrefs.jiraConfig as JiraConfig | null; // Si config en DB, l'utiliser @@ -278,11 +276,11 @@ class UserPreferencesService { /** * Sauvegarde la configuration TFS */ - async saveTfsConfig(config: TfsConfig): Promise { + async saveTfsConfig(userId: string, config: TfsConfig): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); await prisma.userPreferences.update({ - where: { id: userPrefs.id }, + where: { userId }, data: { tfsConfig: config as any }, // eslint-disable-line @typescript-eslint/no-explicit-any }); } catch (error) { @@ -294,9 +292,9 @@ class UserPreferencesService { /** * Récupère la configuration TFS depuis la base de données */ - async getTfsConfig(): Promise { + async getTfsConfig(userId: string): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); const dbConfig = userPrefs.tfsConfig as TfsConfig | null; if ( @@ -319,15 +317,16 @@ class UserPreferencesService { * Sauvegarde les préférences du scheduler TFS */ async saveTfsSchedulerConfig( + userId: string, tfsAutoSync: boolean, tfsSyncInterval: 'hourly' | 'daily' | 'weekly' ): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); await prisma.$executeRaw` UPDATE user_preferences SET tfsAutoSync = ${tfsAutoSync}, tfsSyncInterval = ${tfsSyncInterval} - WHERE id = ${userPrefs.id} + WHERE userId = ${userId} `; } catch (error) { console.warn( @@ -341,16 +340,16 @@ class UserPreferencesService { /** * Récupère les préférences du scheduler TFS */ - async getTfsSchedulerConfig(): Promise<{ + async getTfsSchedulerConfig(userId: string): Promise<{ tfsAutoSync: boolean; tfsSyncInterval: 'hourly' | 'daily' | 'weekly'; }> { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); const result = await prisma.$queryRaw< Array<{ tfsAutoSync: number; tfsSyncInterval: string }> >` - SELECT tfsAutoSync, tfsSyncInterval FROM user_preferences WHERE id = ${userPrefs.id} + SELECT tfsAutoSync, tfsSyncInterval FROM user_preferences WHERE userId = ${userId} `; if (result.length > 0) { @@ -384,16 +383,17 @@ class UserPreferencesService { * Sauvegarde les préférences du scheduler Jira */ async saveJiraSchedulerConfig( + userId: string, jiraAutoSync: boolean, jiraSyncInterval: 'hourly' | 'daily' | 'weekly' ): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); // Utiliser une requête SQL brute temporairement pour éviter les problèmes de types await prisma.$executeRaw` UPDATE user_preferences SET jiraAutoSync = ${jiraAutoSync}, jiraSyncInterval = ${jiraSyncInterval} - WHERE id = ${userPrefs.id} + WHERE userId = ${userId} `; } catch (error) { console.warn( @@ -407,17 +407,17 @@ class UserPreferencesService { /** * Récupère les préférences du scheduler Jira */ - async getJiraSchedulerConfig(): Promise<{ + async getJiraSchedulerConfig(userId: string): Promise<{ jiraAutoSync: boolean; jiraSyncInterval: 'hourly' | 'daily' | 'weekly'; }> { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); // Utiliser une requête SQL brute pour récupérer les nouveaux champs const result = await prisma.$queryRaw< Array<{ jiraAutoSync: number; jiraSyncInterval: string }> >` - SELECT jiraAutoSync, jiraSyncInterval FROM user_preferences WHERE id = ${userPrefs.id} + SELECT jiraAutoSync, jiraSyncInterval FROM user_preferences WHERE userId = ${userPrefs.userId} `; if (result.length > 0) { @@ -445,14 +445,14 @@ class UserPreferencesService { /** * Récupère les préférences utilisateur (alias pour getAllPreferences) */ - async getUserPreferences(): Promise { - return this.getAllPreferences(); + async getUserPreferences(userId: string): Promise { + return this.getAllPreferences(userId); } /** * Récupère toutes les préférences utilisateur */ - async getAllPreferences(): Promise { + async getAllPreferences(userId: string): Promise { const [ kanbanFilters, viewPreferences, @@ -462,13 +462,13 @@ class UserPreferencesService { tfsConfig, tfsSchedulerConfig, ] = await Promise.all([ - this.getKanbanFilters(), - this.getViewPreferences(), - this.getColumnVisibility(), - this.getJiraConfig(), - this.getJiraSchedulerConfig(), - this.getTfsConfig(), - this.getTfsSchedulerConfig(), + this.getKanbanFilters(userId), + this.getViewPreferences(userId), + this.getColumnVisibility(userId), + this.getJiraConfig(userId), + this.getJiraSchedulerConfig(userId), + this.getTfsConfig(userId), + this.getTfsSchedulerConfig(userId), ]); return { @@ -487,18 +487,20 @@ class UserPreferencesService { /** * Sauvegarde toutes les préférences utilisateur */ - async saveAllPreferences(preferences: UserPreferences): Promise { + async saveAllPreferences(userId: string, preferences: UserPreferences): Promise { await Promise.all([ - this.saveKanbanFilters(preferences.kanbanFilters), - this.saveViewPreferences(preferences.viewPreferences), - this.saveColumnVisibility(preferences.columnVisibility), - this.saveJiraConfig(preferences.jiraConfig), + this.saveKanbanFilters(userId, preferences.kanbanFilters), + this.saveViewPreferences(userId, preferences.viewPreferences), + this.saveColumnVisibility(userId, preferences.columnVisibility), + this.saveJiraConfig(userId, preferences.jiraConfig), this.saveJiraSchedulerConfig( + userId, preferences.jiraAutoSync, preferences.jiraSyncInterval ), - this.saveTfsConfig(preferences.tfsConfig), + this.saveTfsConfig(userId, preferences.tfsConfig), this.saveTfsSchedulerConfig( + userId, preferences.tfsAutoSync, preferences.tfsSyncInterval ), @@ -508,11 +510,11 @@ class UserPreferencesService { /** * Remet à zéro toutes les préférences */ - async resetAllPreferences(): Promise { + async resetAllPreferences(userId: string): Promise { try { - const userPrefs = await this.getOrCreateUserPreferences(); + const userPrefs = await this.getOrCreateUserPreferences(userId); await prisma.userPreferences.update({ - where: { id: userPrefs.id }, + where: { userId: userPrefs.userId }, data: { kanbanFilters: DEFAULT_PREFERENCES.kanbanFilters, viewPreferences: DEFAULT_PREFERENCES.viewPreferences, @@ -531,24 +533,24 @@ class UserPreferencesService { /** * Met à jour partiellement les filtres Kanban */ - async updateKanbanFilters(updates: Partial): Promise { - const current = await this.getKanbanFilters(); - await this.saveKanbanFilters({ ...current, ...updates }); + async updateKanbanFilters(userId: string, updates: Partial): Promise { + const current = await this.getKanbanFilters(userId); + await this.saveKanbanFilters(userId, { ...current, ...updates }); } /** * Met à jour partiellement les préférences de vue */ - async updateViewPreferences(updates: Partial): Promise { - const current = await this.getViewPreferences(); - await this.saveViewPreferences({ ...current, ...updates }); + async updateViewPreferences(userId: string, updates: Partial): Promise { + const current = await this.getViewPreferences(userId); + await this.saveViewPreferences(userId, { ...current, ...updates }); } /** * Met à jour la visibilité d'une colonne spécifique */ - async toggleColumnVisibility(status: TaskStatus): Promise { - const current = await this.getColumnVisibility(); + async toggleColumnVisibility(userId: string, status: TaskStatus): Promise { + const current = await this.getColumnVisibility(userId); const hiddenStatuses = new Set(current.hiddenStatuses); if (hiddenStatuses.has(status)) { @@ -557,7 +559,7 @@ class UserPreferencesService { hiddenStatuses.add(status); } - await this.saveColumnVisibility({ + await this.saveColumnVisibility(userId, { hiddenStatuses: Array.from(hiddenStatuses) }); } diff --git a/src/services/data-management/backup.ts b/src/services/data-management/backup.ts index 7b4b2f1..84254ae 100644 --- a/src/services/data-management/backup.ts +++ b/src/services/data-management/backup.ts @@ -55,7 +55,9 @@ export class BackupService { */ private async loadConfigFromDB(): Promise { try { - const preferences = await userPreferencesService.getAllPreferences(); + // Pour le service de backup, on utilise un userId par défaut + // car il n'a pas accès à la session + const preferences = await userPreferencesService.getAllPreferences('default'); if (preferences.viewPreferences && typeof preferences.viewPreferences === 'object') { const backupConfig = (preferences.viewPreferences as Record).backupConfig; if (backupConfig) { @@ -75,15 +77,15 @@ export class BackupService { // Pour l'instant, on stocke la config backup en tant que JSON dans viewPreferences // TODO: Ajouter un champ dédié dans le schéma pour la config backup await prisma.userPreferences.upsert({ - where: { id: 'default' }, + where: { userId: 'default' }, update: { viewPreferences: JSON.parse(JSON.stringify({ - ...(await userPreferencesService.getViewPreferences()), + ...(await userPreferencesService.getViewPreferences('default')), backupConfig: this.config })) }, create: { - id: 'default', + userId: 'default', kanbanFilters: {}, viewPreferences: JSON.parse(JSON.stringify({ backupConfig: this.config })), columnVisibility: {}, diff --git a/src/services/integrations/jira/scheduler.ts b/src/services/integrations/jira/scheduler.ts index 55d954d..47709e4 100644 --- a/src/services/integrations/jira/scheduler.ts +++ b/src/services/integrations/jira/scheduler.ts @@ -28,7 +28,8 @@ export class JiraScheduler { } // Vérifier que Jira est configuré - const jiraConfig = await userPreferencesService.getJiraConfig(); + // Pour les services système, on utilise un userId par défaut + const jiraConfig = await userPreferencesService.getJiraConfig('default'); if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { console.log('⚠️ Jira not configured, scheduler cannot start'); return; @@ -84,7 +85,7 @@ export class JiraScheduler { console.log('🔄 Starting scheduled Jira sync...'); // Récupérer la config Jira - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig('default'); if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { console.log('⚠️ Jira config incomplete, skipping scheduled sync'); @@ -154,8 +155,8 @@ export class JiraScheduler { private async getConfig(): Promise { try { const [jiraConfig, schedulerConfig] = await Promise.all([ - userPreferencesService.getJiraConfig(), - userPreferencesService.getJiraSchedulerConfig() + userPreferencesService.getJiraConfig('default'), + userPreferencesService.getJiraSchedulerConfig('default') ]); return { @@ -180,7 +181,7 @@ export class JiraScheduler { */ async getStatus() { const config = await this.getConfig(); - const jiraConfig = await userPreferencesService.getJiraConfig(); + const jiraConfig = await userPreferencesService.getJiraConfig('default'); return { isRunning: this.isRunning, diff --git a/src/services/integrations/tfs.ts b/src/services/integrations/tfs.ts index 6fc0836..1cc4cfa 100644 --- a/src/services/integrations/tfs.ts +++ b/src/services/integrations/tfs.ts @@ -1054,7 +1054,7 @@ class TfsServiceInstance extends TfsService { } private async getConfig(): Promise { - const userConfig = await userPreferencesService.getTfsConfig(); + const userConfig = await userPreferencesService.getTfsConfig('default'); return userConfig; }