espace-paie-odentas/app/api/search/route.ts
2025-10-12 17:05:46 +02:00

255 lines
10 KiB
TypeScript

// app/api/search/route.ts - Production-safe
import { NextRequest, NextResponse } from "next/server";
import { cookies, headers } from "next/headers";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { detectDemoModeFromHeaders } from "@/lib/demo-detector";
import { searchDemoData } from "@/lib/demo-data";
// Avoid static optimization/caching in production for authenticated search
export const dynamic = 'force-dynamic';
export const revalidate = 0;
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const q = searchParams.get("q")?.trim() ?? "";
const limitParam = searchParams.get("limit");
const limit = limitParam ? Math.max(1, Math.min(50, parseInt(limitParam, 10))) : 20;
const debugMode = searchParams.get("debug") === "true";
if (q.length < 2) {
return NextResponse.json(
{ error: "Query parameter 'q' is required and must be at least 2 characters long." },
{ status: 400 }
);
}
// 🎭 Vérification du mode démo en premier
const h = headers();
const isDemoMode = detectDemoModeFromHeaders(h);
if (isDemoMode) {
console.log("🎭 [API SEARCH] Mode démo détecté - recherche dans les données fictives");
const results = searchDemoData(q);
return NextResponse.json({
results: results.slice(0, limit),
total: results.length,
query: q,
demo: true
});
}
try {
const supabase = createRouteHandlerClient({ cookies });
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
if (sessionError || !session) {
console.error("Auth error in search API:", sessionError);
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Récupérer l'organisation de l'utilisateur
let userOrgId: string | null = null;
console.log("User ID:", session.user.id);
try {
const { data: member, error: mErr } = await supabase
.from("organization_members")
.select("org_id")
.eq("user_id", session.user.id)
.single();
console.log("Organization member query result:", { member, error: mErr });
if (!mErr && member?.org_id) {
userOrgId = member.org_id;
console.log("Found user org_id:", userOrgId);
} else {
// If no membership, check if the user is a staff user and allow global access for staff
try {
const { data: s } = await supabase.from('staff_users').select('is_staff').eq('user_id', session.user.id).maybeSingle();
const isStaff = !!s?.is_staff;
console.log("Staff check result:", { staff_data: s, isStaff });
if (!isStaff) {
console.warn("Utilisateur sans organisation associée:", session.user.id);
return NextResponse.json({ error: "No organization found" }, { status: 403 });
}
// staff: leave userOrgId as null to indicate global access
console.log("Search: staff user without org - proceeding with global search");
} catch (e) {
console.error("Erreur lors de la vérification staff:", e);
return NextResponse.json({ error: "Organization lookup failed" }, { status: 500 });
}
}
} catch (err) {
console.error("Erreur lors de la récupération de l'organisation:", err);
return NextResponse.json({ error: "Organization lookup failed" }, { status: 500 });
}
console.log("Recherche pour:", q, "- org_id:", userOrgId);
// Debug: Vérifier qu'il y a bien des données pour cette org
const { data: sampleData, error: sampleError } = await supabase
.from("search_index")
.select("id, entity_type, title, searchable, org_id")
.eq("org_id", userOrgId)
.limit(5);
console.log("Échantillon de données pour cette org:", sampleData?.length, "résultats");
if (sampleError) {
console.error("Erreur échantillon:", sampleError);
}
if (sampleData && sampleData.length > 0) {
console.log("Premier échantillon:", {
id: sampleData[0].id,
org_id: sampleData[0].org_id,
title: sampleData[0].title,
searchable: sampleData[0].searchable?.substring(0, 100) + "..."
});
} else {
// Si pas de données pour cette org, vérifions toutes les orgs
console.log("Pas de données pour org_id:", userOrgId);
// Test avec service role pour voir si c'est un problème de RLS
const { createClient } = await import('@supabase/supabase-js');
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
const { data: adminData, error: adminError } = await supabaseAdmin
.from("search_index")
.select("id, org_id, title")
.limit(5);
console.log("Test avec service role - Données disponibles:", adminData?.length || 0);
if (adminError) {
console.error("Erreur service role:", adminError);
}
if (adminData && adminData.length > 0) {
const uniqueOrgs = [...new Set(adminData.map(d => d.org_id))];
console.log("Organisations trouvées avec service role:", uniqueOrgs);
// Chercher spécifiquement DEVALAN avec service role
const { data: devalanAdmin } = await supabaseAdmin
.from("search_index")
.select("id, org_id, title")
.ilike("title", "%devalan%")
.limit(3);
console.log("DEVALAN trouvé avec service role:", devalanAdmin?.map(d => ({ title: d.title, org_id: d.org_id })));
}
// Test normal
const { data: allOrgs } = await supabase
.from("search_index")
.select("org_id")
.limit(10);
const uniqueOrgs = [...new Set(allOrgs?.map(d => d.org_id) || [])];
console.log("Organisations disponibles dans search_index:", uniqueOrgs);
// Test spécifique : chercher les entrées qui contiennent "devalan"
const { data: devalanTest } = await supabase
.from("search_index")
.select("id, org_id, title")
.ilike("title", "%devalan%")
.limit(3);
console.log("Entrées contenant 'devalan' (toutes orgs):", devalanTest?.map(d => ({ title: d.title, org_id: d.org_id })));
}
// Debug: Test ILIKE simple dès le début pour voir si le problème vient du FTS
console.log("0. Test ILIKE simple pour debug:", q);
const { data: testData, error: testError } = await supabase
.from("search_index")
.select("id, entity_type, title")
.eq("org_id", userOrgId)
.ilike("title", `%${q}%`)
.limit(3);
console.log("0. Test ILIKE résultats:", testData?.length || 0, "- Erreur:", testError?.message);
if (testData && testData.length > 0) {
console.log("0. Premier résultat ILIKE:", testData[0].title);
}
// Helper to apply org filter only when present
const applyOrg = (q: any) => {
console.log("applyOrg called with userOrgId:", userOrgId);
if (userOrgId) {
console.log("Applying org filter: org_id =", userOrgId);
return q.eq("org_id", userOrgId);
} else {
console.log("No org filter applied (global search)");
return q;
}
};
// Approche 1: Recherche FTS simple
console.log("1. Tentative FTS simple:", q);
let query = supabase
.from("search_index")
.select("id, org_id, entity_type, entity_id, title, subtitle, url, icon, updated_at, meta")
.textSearch("searchable", q, { config: "french", type: "plain" });
query = applyOrg(query);
let { data, error } = await query.order("updated_at", { ascending: false }).limit(limit);
console.log("1. Résultats FTS simple:", data?.length || 0, "- Erreur:", error?.message);
// Si pas de résultats, essayer avec préfixes
if (!error && (!data || data.length === 0)) {
console.log("Aucun résultat avec recherche simple, essai avec préfixes");
const searchTerms = q.split(/\s+/).map(term => term.trim()).filter(term => term.length > 0);
const prefixQuery = searchTerms.map(term => `${term}:*`).join(' & ');
{
let q2 = supabase
.from("search_index")
.select("id, org_id, entity_type, entity_id, title, subtitle, url, icon, updated_at, meta")
.textSearch("searchable", prefixQuery, { config: "french", type: "websearch" });
q2 = applyOrg(q2);
({ data, error } = await q2.order("updated_at", { ascending: false }).limit(limit));
}
}
// Si toujours pas de résultats, essayer avec configuration 'simple'
if (!error && (!data || data.length === 0)) {
console.log("Aucun résultat avec préfixes français, essai avec config 'simple'");
const searchTerms = q.split(/\s+/).map(term => term.trim()).filter(term => term.length > 0);
const prefixQuery = searchTerms.map(term => `${term}:*`).join(' & ');
{
let q3 = supabase
.from("search_index")
.select("id, org_id, entity_type, entity_id, title, subtitle, url, icon, updated_at, meta")
.textSearch("searchable", prefixQuery, { config: "simple", type: "websearch" });
q3 = applyOrg(q3);
({ data, error } = await q3.order("updated_at", { ascending: false }).limit(limit));
}
}
// Fallback ILIKE final
if (!error && (!data || data.length === 0)) {
console.log("Aucun résultat avec FTS, essai avec ILIKE sur title");
{
let q4 = supabase
.from("search_index")
.select("id, org_id, entity_type, entity_id, title, subtitle, url, icon, updated_at, meta")
.ilike("title", `%${q}%`);
q4 = applyOrg(q4);
({ data, error } = await q4.order("updated_at", { ascending: false }).limit(limit));
}
}
console.log("Résultats trouvés:", data?.length || 0);
if (error) {
console.error("Search error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data ?? []);
} catch (err: any) {
console.error("API search error:", err);
return NextResponse.json({ error: err?.message ?? "server_error" }, { status: 500 });
}
}