255 lines
10 KiB
TypeScript
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 });
|
|
}
|
|
}
|