diff --git a/src/app/actualites/page.tsx b/src/app/actualites/page.tsx index 261e7d6..f2439fc 100644 --- a/src/app/actualites/page.tsx +++ b/src/app/actualites/page.tsx @@ -15,7 +15,8 @@ const CATEGORY_COLORS: Record = { Sciences: '#38bdf8', } -function formatDate(date: string) { +function formatDate(date: string | null) { + if (!date) return 'Date inconnue' return new Date(date).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', @@ -174,7 +175,7 @@ export default function ActualitesPage() { {article.category} - +

{article.title}

diff --git a/src/app/api/geocode/route.ts b/src/app/api/geocode/route.ts index e53f6e8..67f4e2d 100644 --- a/src/app/api/geocode/route.ts +++ b/src/app/api/geocode/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' +import { checkDistributedRateLimit } from '@/lib/security/rate-limit' const FALLBACK_CITY = 'Votre position' @@ -8,6 +9,21 @@ function parseCoordinate(value: string | null, min: number, max: number) { return Number(parsed.toFixed(2)) } +function firstHeaderValue(request: NextRequest, names: string[]): string | null { + for (const name of names) { + const value = request.headers.get(name)?.split(',')[0]?.trim() + if (value) return value + } + return null +} + +function clientIdentifier(request: NextRequest): string | null { + const headerNames = process.env.VERCEL === '1' + ? ['x-vercel-forwarded-for', 'x-forwarded-for', 'x-real-ip'] + : ['x-forwarded-for', 'x-real-ip'] + return firstHeaderValue(request, headerNames) +} + export async function GET(request: NextRequest) { const latitude = parseCoordinate(request.nextUrl.searchParams.get('lat'), -90, 90) const longitude = parseCoordinate(request.nextUrl.searchParams.get('lon'), -180, 180) @@ -16,6 +32,38 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Coordonnées invalides.' }, { status: 400 }) } + const identifier = clientIdentifier(request) + if (process.env.VERCEL === '1' && !identifier) { + return NextResponse.json( + { error: 'Service de localisation temporairement protégé.' }, + { status: 503, headers: { 'Cache-Control': 'no-store' } }, + ) + } + + const rateLimit = await checkDistributedRateLimit(`geocode:${identifier || 'local-anonymous'}`, { + namespace: 'geocode', + limit: 20, + windowSeconds: 60, + }) + if (rateLimit.unavailable) { + return NextResponse.json( + { error: 'Service de localisation temporairement protégé.' }, + { status: 503, headers: { 'Cache-Control': 'no-store' } }, + ) + } + if (!rateLimit.allowed) { + return NextResponse.json( + { error: 'Trop de demandes de localisation. Réessaie dans un instant.' }, + { + status: 429, + headers: { + 'Cache-Control': 'no-store', + 'Retry-After': String(rateLimit.retryAfter), + }, + }, + ) + } + const url = new URL('https://nominatim.openstreetmap.org/reverse') url.search = new URLSearchParams({ lat: String(latitude), diff --git a/src/app/confidentialite/page.tsx b/src/app/confidentialite/page.tsx index e31d59f..7bc589b 100644 --- a/src/app/confidentialite/page.tsx +++ b/src/app/confidentialite/page.tsx @@ -36,9 +36,15 @@ export default function PrivacyPage() {

Stockage local et intelligence artificielle

- Le thème clair ou sombre est mémorisé uniquement dans votre navigateur. Les questions adressées à - SolarBot sont envoyées au service Gemini de Google pour produire une réponse, sans être conservées - dans une base de données SolarScope. + SolarScope ne crée pas de compte. Le navigateur peut conserver la langue choisie, le public ou niveau + sélectionné, la progression du passeport, l’étape ouverte dans une fiche pédagogique et un cache local + temporaire de météo spatiale. Ces informations restent sur l’appareil et ne sont pas envoyées à SolarScope. +

+

+ Le passeport peut être effacé depuis la page « Passeport spatial ». Les autres préférences et le cache + peuvent être supprimés depuis les réglages de stockage du navigateur ; le cache météo expire également + automatiquement. Les questions adressées à SolarBot sont envoyées au service Gemini de Google lorsque + Gemini est activé, sans être conservées dans une base de données SolarScope.

← Retour à l’accueil

diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index 704f7cb..feb36b2 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -382,10 +382,14 @@ export default function Navbar() { {navGroups.map(group => { const isGroupOpen = mobileGroup === group.id const isActive = activeGroup === group.id + const mobileGroupPanelId = `mobile-navigation-group-${group.id}` return (