Files
afrov2/app/api/businesses/route.ts
streaper2 f450f8ee03
All checks were successful
Build and Push App / build (push) Successful in 5m50s
correction faille de securité
2026-05-01 21:48:00 +02:00

193 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { USER_SAFE_SELECT, getAuthUser } from '@/lib/auth-utils'
// GET /api/businesses — List all businesses (Mock + DB)
export async function GET(request: NextRequest) {
try {
const user = await getAuthUser(request)
if (!user) {
return NextResponse.json({ error: 'Authentification requise' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const category = searchParams.get('category')
const q = searchParams.get('q')?.toLowerCase()
const featured = searchParams.get('featured')
const countryId = searchParams.get('countryId')
// 1. Fetch from Database
const where: any = {
isActive: true,
isSuspended: false,
owner: {
isSuspended: false
}
}
if (category && category !== 'All') {
// Robust check: IDs are usually alphanumeric and long, without spaces or special chars
// Names like "Agriculture & Agrobusiness" have spaces or special chars.
const isId = /^[a-z0-9-]+$/.test(category) && category.length > 20;
if (isId) {
where.categoryId = category
} else {
where.category = category
}
}
if (featured === 'true') where.isFeatured = true
if (countryId) where.countryId = countryId
if (q) {
where.OR = [
{ name: { contains: q, mode: 'insensitive' } },
{ description: { contains: q, mode: 'insensitive' } },
// Simplified tags check
{ tags: { has: q } }
]
}
const dbBusinesses = await prisma.business.findMany({
where,
include: {
owner: {
select: USER_SAFE_SELECT
},
offers: true
},
orderBy: { createdAt: 'desc' },
})
// 2. Map results to mask technical IDs
const safeBusinesses = dbBusinesses.map(biz => {
const { id, ownerId, ...rest } = biz
return {
...rest,
// Return slug as the primary identifier if available,
// otherwise return a masked version or keep it but it's now "safe"
id: biz.slug || id
}
})
return NextResponse.json(safeBusinesses)
} catch (error) {
console.error('GET /api/businesses error:', error)
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const headerUserId = request.headers.get('x-user-id')
const { ownerId: bodyOwnerId, ...data } = body
const ownerId = bodyOwnerId || headerUserId
if (!ownerId) {
return NextResponse.json({ error: 'ownerId est requis (via body ou x-user-id header)' }, { status: 400 })
}
// 1. Sanitize & Validate data
const cleanData: any = {
name: data.name || "",
slug: data.slug || null,
category: data.category || "Autre",
categoryId: (data.categoryId && data.categoryId !== 'Autre') ? data.categoryId : null,
suggestedCategory: data.suggestedCategory || null,
location: data.location || "",
countryId: data.countryId || null,
city: data.city || "",
description: data.description || "",
logoUrl: data.logoUrl || "https://picsum.photos/200/200?random=default",
coverUrl: data.coverUrl || null,
coverPosition: data.coverPosition || "50% 50%",
coverZoom: data.coverZoom ? parseFloat(data.coverZoom) : 1.0,
videoUrl: data.videoUrl || null,
contactEmail: data.contactEmail || "",
contactPhone: data.contactPhone || null,
websiteUrl: data.websiteUrl || null,
socialLinks: data.socialLinks || {},
showEmail: data.showEmail ?? true,
showPhone: data.showPhone ?? true,
showSocials: data.showSocials ?? true,
verified: data.verified ?? false,
viewCount: data.viewCount ?? 0,
rating: data.rating ?? 0,
tags: Array.isArray(data.tags) ? data.tags : [],
isFeatured: data.isFeatured ?? false,
founderName: data.founderName || null,
founderImageUrl: data.founderImageUrl || null,
keyMetric: data.keyMetric || null,
}
// 1b. Calculate isActive based on mandatory fields
const isNameOk = cleanData.name && cleanData.name !== "Nouvelle Entreprise";
const isDescOk = cleanData.description && cleanData.description.length >= 20;
const isLocOk = (cleanData.countryId && cleanData.city) || (cleanData.location && cleanData.location !== "Ma Ville");
const isEmailOk = !!cleanData.contactEmail;
const isPhoneOk = !!cleanData.contactPhone;
cleanData.isActive = !!(isNameOk && isDescOk && isLocOk && isEmailOk && isPhoneOk);
// 2. Slug Uniqueness Check
if (cleanData.slug) {
const slugConflict = await prisma.business.findFirst({
where: {
slug: cleanData.slug,
NOT: { ownerId: ownerId } // Important: allow the owner to keep their own slug
}
});
if (slugConflict) {
return NextResponse.json({
error: `Le lien personnalisé "${cleanData.slug}" est déjà utilisé par une autre entreprise. Veuillez en choisir un autre.`
}, { status: 400 });
}
}
let business;
const existing = await prisma.business.findFirst({
where: { ownerId: ownerId }
});
console.log('Upserting business for ownerId:', ownerId, 'Existing:', !!existing);
if (existing) {
// When updating, we use the sanitized cleanData and the existing record's id
business = await prisma.business.update({
where: { id: existing.id },
data: cleanData,
include: {
owner: {
select: USER_SAFE_SELECT
}
}
});
} else {
// When creating, we add the ownerId to the sanitized cleanData
business = await prisma.business.create({
data: {
...cleanData,
ownerId: ownerId
},
include: {
owner: {
select: USER_SAFE_SELECT
}
}
});
// Automatic role upgrade
await prisma.user.update({
where: { id: ownerId },
data: { role: 'ENTREPRENEUR' }
});
}
console.log('Business saved successfully:', business.id);
return NextResponse.json(business, { status: 200 })
} catch (error) {
console.error('POST /api/businesses error:', error)
return NextResponse.json({ error: 'Erreur lors de lenregistrement' }, { status: 500 })
}
}