correction faille de securité
All checks were successful
Build and Push App / build (push) Successful in 5m50s

This commit is contained in:
2026-05-01 21:48:00 +02:00
parent 9c003d1b7d
commit f450f8ee03
10 changed files with 78 additions and 19 deletions

View File

@@ -77,7 +77,9 @@ const BusinessDetailPage = () => {
// 2. Try API (Database) // 2. Try API (Database)
try { try {
const res = await fetch(`/api/businesses/${id}`); const res = await fetch(`/api/businesses/${id}`, {
headers: user ? { 'x-user-id': user.id } : {}
});
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
setBusiness(data); setBusiness(data);
@@ -104,7 +106,10 @@ const BusinessDetailPage = () => {
} }
// Also increment views count in DB (Legacy support) // Also increment views count in DB (Legacy support)
fetch(`/api/businesses/${id}/view`, { method: 'POST' }).catch(console.error); fetch(`/api/businesses/${id}/view`, {
method: 'POST',
headers: user ? { 'x-user-id': user.id } : {}
}).catch(console.error);
} else { } else {
setBusiness(null); setBusiness(null);
} }
@@ -135,7 +140,9 @@ const BusinessDetailPage = () => {
// 5. Fetch all ratings // 5. Fetch all ratings
try { try {
setLoadingRatings(true); setLoadingRatings(true);
const ratingsRes = await fetch(`/api/businesses/${id}/ratings`); const ratingsRes = await fetch(`/api/businesses/${id}/ratings`, {
headers: user ? { 'x-user-id': user.id } : {}
});
if (ratingsRes.ok) { if (ratingsRes.ok) {
const ratingsData = await ratingsRes.json(); const ratingsData = await ratingsRes.json();
setRatings(ratingsData); setRatings(ratingsData);

View File

@@ -7,8 +7,10 @@ import { Search, Loader2 } from 'lucide-react';
import { Business, Country } from '../../types'; import { Business, Country } from '../../types';
import BusinessCard from '../../components/BusinessCard'; import BusinessCard from '../../components/BusinessCard';
import AnnuaireHero from '../../components/AnnuaireHero'; import AnnuaireHero from '../../components/AnnuaireHero';
import { useUser } from '../../components/UserProvider';
const AnnuairePageContent = () => { const AnnuairePageContent = () => {
const { user } = useUser();
const [businesses, setBusinesses] = useState<Business[]>([]); const [businesses, setBusinesses] = useState<Business[]>([]);
const [featuredBusiness, setFeaturedBusiness] = useState<Business | null>(null); const [featuredBusiness, setFeaturedBusiness] = useState<Business | null>(null);
const [countries, setCountries] = useState<Country[]>([]); const [countries, setCountries] = useState<Country[]>([]);
@@ -41,7 +43,10 @@ const AnnuairePageContent = () => {
const fetchHeadliner = async () => { const fetchHeadliner = async () => {
try { try {
// Disable caching to ensure we get fresh data and more variety // Disable caching to ensure we get fresh data and more variety
const res = await fetch('/api/businesses?featured=true', { cache: 'no-store' }); const res = await fetch('/api/businesses?featured=true', {
cache: 'no-store',
headers: user ? { 'x-user-id': user.id } : {}
});
const data = await res.json(); const data = await res.json();
if (Array.isArray(data) && data.length > 0) { if (Array.isArray(data) && data.length > 0) {
// Truly random selection from the pool of featured businesses // Truly random selection from the pool of featured businesses
@@ -65,7 +70,9 @@ const AnnuairePageContent = () => {
if (filterCountry !== 'All') params.append('countryId', filterCountry); if (filterCountry !== 'All') params.append('countryId', filterCountry);
if (searchQuery) params.append('q', searchQuery); if (searchQuery) params.append('q', searchQuery);
const res = await fetch(`/api/businesses?${params.toString()}`); const res = await fetch(`/api/businesses?${params.toString()}`, {
headers: user ? { 'x-user-id': user.id } : {}
});
const data = await res.json(); const data = await res.json();
if (Array.isArray(data)) { if (Array.isArray(data)) {
setBusinesses(data); setBusinesses(data);

View File

@@ -1,15 +1,20 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { USER_SAFE_SELECT } from '@/lib/auth-utils' import { USER_SAFE_SELECT, getAuthUser } from '@/lib/auth-utils'
type Params = { params: Promise<{ id: string }> } type Params = { params: Promise<{ id: string }> }
// GET /api/businesses/[id] // GET /api/businesses/[id]
export async function GET( export async function GET(
_request: NextRequest, request: NextRequest,
context: { params: Promise<{ id: string }> } context: { params: Promise<{ id: string }> }
): Promise<Response> { ): Promise<Response> {
try { try {
const user = await getAuthUser(request)
if (!user) {
return NextResponse.json({ error: 'Authentification requise' }, { status: 401 })
}
const { id } = await context.params const { id } = await context.params
const business = await prisma.business.findFirst({ const business = await prisma.business.findFirst({
where: { where: {
@@ -26,7 +31,13 @@ export async function GET(
}, },
}) })
if (!business) return NextResponse.json({ error: 'Non trouvé' }, { status: 404 }) if (!business) return NextResponse.json({ error: 'Non trouvé' }, { status: 404 })
return NextResponse.json(business)
// Mask technical IDs
const { id: bizId, ownerId, ...rest } = business
return NextResponse.json({
...rest,
id: business.slug || bizId
})
} catch (error) { } catch (error) {
console.error('GET /api/businesses/[id] error:', error) console.error('GET /api/businesses/[id] error:', error)
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 }) return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })

View File

@@ -1,10 +1,15 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { USER_SAFE_SELECT } from '@/lib/auth-utils' import { USER_SAFE_SELECT, getAuthUser } from '@/lib/auth-utils'
// GET /api/businesses — List all businesses (Mock + DB) // GET /api/businesses — List all businesses (Mock + DB)
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const user = await getAuthUser(request)
if (!user) {
return NextResponse.json({ error: 'Authentification requise' }, { status: 401 })
}
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const category = searchParams.get('category') const category = searchParams.get('category')
const q = searchParams.get('q')?.toLowerCase() const q = searchParams.get('q')?.toLowerCase()
@@ -52,8 +57,18 @@ export async function GET(request: NextRequest) {
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
}) })
// 2. Return DB results // 2. Map results to mask technical IDs
return NextResponse.json(dbBusinesses) 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) { } catch (error) {
console.error('GET /api/businesses error:', error) console.error('GET /api/businesses error:', error)
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 }) return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })

View File

@@ -43,12 +43,29 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'businessId requis' }, { status: 400 }); return NextResponse.json({ error: 'businessId requis' }, { status: 400 });
} }
// Resolve business ID (in case slug was passed)
const business = await prisma.business.findFirst({
where: {
OR: [
{ id: businessId },
{ slug: businessId }
]
},
select: { id: true }
});
if (!business) {
return NextResponse.json({ error: 'Entreprise non trouvée' }, { status: 404 });
}
const actualBusinessId = business.id;
// Check if already favorited // Check if already favorited
const existing = await prisma.favorite.findUnique({ const existing = await prisma.favorite.findUnique({
where: { where: {
userId_businessId: { userId_businessId: {
userId, userId,
businessId businessId: actualBusinessId
} }
} }
}); });
@@ -64,7 +81,7 @@ export async function POST(request: NextRequest) {
await prisma.favorite.create({ await prisma.favorite.create({
data: { data: {
userId, userId,
businessId businessId: actualBusinessId
} }
}); });
return NextResponse.json({ favorited: true }); return NextResponse.json({ favorited: true });

View File

@@ -72,7 +72,7 @@ const ForgotPasswordPage = () => {
</div> </div>
</div> </div>
) : ( ) : (
<form className="mt-8 space-y-6" onSubmit={handleSubmit}> <form className="mt-8 space-y-6" onSubmit={handleSubmit} method="POST">
<input type="hidden" name="csrf_token" value={csrfToken} /> <input type="hidden" name="csrf_token" value={csrfToken} />
{error && ( {error && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded text-red-700 text-sm"> <div className="bg-red-50 border-l-4 border-red-500 p-4 rounded text-red-700 text-sm">

View File

@@ -145,7 +145,7 @@ const LoginPage = () => {
</div> </div>
)} )}
<form className="space-y-6" onSubmit={handleSubmit}> <form className="space-y-6" onSubmit={handleSubmit} method="POST">
<input type="hidden" name="csrf_token" value={csrfToken} /> <input type="hidden" name="csrf_token" value={csrfToken} />
<div className="rounded-md shadow-sm -space-y-px"> <div className="rounded-md shadow-sm -space-y-px">
<div> <div>

View File

@@ -11,7 +11,7 @@ import { useUser } from '../components/UserProvider';
const HomePage = () => { const HomePage = () => {
const router = useRouter(); const router = useRouter();
const { settings } = useUser(); const { user, settings } = useUser();
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [featuredBusinesses, setFeaturedBusinesses] = useState<Business[]>([]); const [featuredBusinesses, setFeaturedBusinesses] = useState<Business[]>([]);
const [posts, setPosts] = useState<BlogPost[]>([]); const [posts, setPosts] = useState<BlogPost[]>([]);
@@ -22,7 +22,9 @@ const HomePage = () => {
useEffect(() => { useEffect(() => {
const fetchFeatured = async () => { const fetchFeatured = async () => {
try { try {
const res = await fetch('/api/businesses?featured=true'); const res = await fetch('/api/businesses?featured=true', {
headers: user ? { 'x-user-id': user.id } : {}
});
const data = await res.json(); const data = await res.json();
if (Array.isArray(data)) { if (Array.isArray(data)) {
setFeaturedBusinesses(data.slice(0, 4)); // Show top 4 setFeaturedBusinesses(data.slice(0, 4)); // Show top 4

View File

@@ -98,7 +98,7 @@ const RegisterPage = () => {
<p className="text-gray-600">Redirection vers la page de connexion...</p> <p className="text-gray-600">Redirection vers la page de connexion...</p>
</div> </div>
) : ( ) : (
<form className="space-y-6" onSubmit={handleSubmit}> <form className="space-y-6" onSubmit={handleSubmit} method="POST">
<input type="hidden" name="csrf_token" value={csrfToken} /> <input type="hidden" name="csrf_token" value={csrfToken} />
{error && ( {error && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded-r-lg flex items-center gap-3 text-red-700 animate-in slide-in-from-top-2 duration-300"> <div className="bg-red-50 border-l-4 border-red-500 p-4 rounded-r-lg flex items-center gap-3 text-red-700 animate-in slide-in-from-top-2 duration-300">

View File

@@ -95,7 +95,7 @@ const ResetPasswordForm = () => {
<p className="mt-2">Redirection vers la page de connexion...</p> <p className="mt-2">Redirection vers la page de connexion...</p>
</div> </div>
) : ( ) : (
<form className="mt-8 space-y-6" onSubmit={handleSubmit}> <form className="mt-8 space-y-6" onSubmit={handleSubmit} method="POST">
<input type="hidden" name="csrf_token" value={csrfToken} /> <input type="hidden" name="csrf_token" value={csrfToken} />
{error && ( {error && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded text-red-700 text-sm"> <div className="bg-red-50 border-l-4 border-red-500 p-4 rounded text-red-700 text-sm">