diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 7db48cd..aa66cfe 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -76,6 +76,11 @@ ONE_API_URL=https://api.one-line.com/v1 ONE_USERNAME=your-one-username ONE_PASSWORD=your-one-password +# Swagger Documentation Access (HTTP Basic Auth) +# Leave empty to disable Swagger in production, or set both to protect with a password +SWAGGER_USERNAME=admin +SWAGGER_PASSWORD=change-this-strong-password + # Security BCRYPT_ROUNDS=12 SESSION_TIMEOUT_MS=7200000 diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index ed82eae..657f47a 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -7,6 +7,7 @@ import compression from 'compression'; import { AppModule } from './app.module'; import { Logger } from 'nestjs-pino'; import { helmetConfig, corsConfig } from './infrastructure/security/security.config'; +import type { Request, Response, NextFunction } from 'express'; async function bootstrap() { const app = await NestFactory.create(AppModule, { @@ -19,6 +20,7 @@ async function bootstrap() { const configService = app.get(ConfigService); const port = configService.get('PORT', 4000); const apiPrefix = configService.get('API_PREFIX', 'api/v1'); + const isProduction = configService.get('NODE_ENV') === 'production'; // Use Pino logger app.useLogger(app.get(Logger)); @@ -52,39 +54,76 @@ async function bootstrap() { }) ); - // Swagger documentation - const config = new DocumentBuilder() - .setTitle('Xpeditis API') - .setDescription( - 'Maritime Freight Booking Platform - API for searching rates and managing bookings' - ) - .setVersion('1.0') - .addBearerAuth() - .addTag('rates', 'Rate search and comparison') - .addTag('bookings', 'Booking management') - .addTag('auth', 'Authentication and authorization') - .addTag('users', 'User management') - .addTag('organizations', 'Organization management') - .build(); + // ─── Swagger documentation ──────────────────────────────────────────────── + const swaggerUser = configService.get('SWAGGER_USERNAME'); + const swaggerPass = configService.get('SWAGGER_PASSWORD'); + const swaggerEnabled = !isProduction || (Boolean(swaggerUser) && Boolean(swaggerPass)); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api/docs', app, document, { - customSiteTitle: 'Xpeditis API Documentation', - customfavIcon: 'https://xpeditis.com/favicon.ico', - customCss: '.swagger-ui .topbar { display: none }', - }); + if (swaggerEnabled) { + // HTTP Basic Auth guard for Swagger routes when credentials are configured + if (swaggerUser && swaggerPass) { + const swaggerPaths = ['/api/docs', '/api/docs-json', '/api/docs-yaml']; + app.use(swaggerPaths, (req: Request, res: Response, next: NextFunction) => { + const authHeader = req.headers['authorization']; + if (!authHeader || !authHeader.startsWith('Basic ')) { + res.setHeader('WWW-Authenticate', 'Basic realm="Xpeditis API Docs"'); + res.status(401).send('Authentication required'); + return; + } + const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf-8'); + const colonIndex = decoded.indexOf(':'); + const user = decoded.slice(0, colonIndex); + const pass = decoded.slice(colonIndex + 1); + if (user !== swaggerUser || pass !== swaggerPass) { + res.setHeader('WWW-Authenticate', 'Basic realm="Xpeditis API Docs"'); + res.status(401).send('Invalid credentials'); + return; + } + next(); + }); + } + + const config = new DocumentBuilder() + .setTitle('Xpeditis API') + .setDescription( + 'Maritime Freight Booking Platform - API for searching rates and managing bookings' + ) + .setVersion('1.0') + .addBearerAuth() + .addApiKey({ type: 'apiKey', name: 'x-api-key', in: 'header' }, 'x-api-key') + .addTag('rates', 'Rate search and comparison') + .addTag('bookings', 'Booking management') + .addTag('auth', 'Authentication and authorization') + .addTag('users', 'User management') + .addTag('organizations', 'Organization management') + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api/docs', app, document, { + customSiteTitle: 'Xpeditis API Documentation', + customfavIcon: 'https://xpeditis.com/favicon.ico', + customCss: '.swagger-ui .topbar { display: none }', + }); + } + // ───────────────────────────────────────────────────────────────────────── await app.listen(port); + const swaggerStatus = swaggerEnabled + ? swaggerUser + ? `http://localhost:${port}/api/docs (protected)` + : `http://localhost:${port}/api/docs (open — add SWAGGER_USERNAME/PASSWORD to secure)` + : 'disabled in production'; + console.log(` - ╔═══════════════════════════════════════╗ - ║ ║ - ║ 🚢 Xpeditis API Server Running ║ - ║ ║ - ║ API: http://localhost:${port}/${apiPrefix} ║ - ║ Docs: http://localhost:${port}/api/docs ║ - ║ ║ - ╚═══════════════════════════════════════╝ + ╔═══════════════════════════════════════════════╗ + ║ ║ + ║ 🚢 Xpeditis API Server Running ║ + ║ ║ + ║ API: http://localhost:${port}/${apiPrefix} ║ + ║ Docs: ${swaggerStatus} ║ + ║ ║ + ╚═══════════════════════════════════════════════╝ `); } diff --git a/apps/frontend/app/dashboard/docs/page.tsx b/apps/frontend/app/dashboard/docs/page.tsx index 6816fcf..467006d 100644 --- a/apps/frontend/app/dashboard/docs/page.tsx +++ b/apps/frontend/app/dashboard/docs/page.tsx @@ -1,1113 +1,7 @@ 'use client'; -import { useState, useEffect, Suspense } from 'react'; -import { useSearchParams, useRouter } from 'next/navigation'; -import { - Search, - ChevronRight, - ArrowRight, - ExternalLink, - Zap, - Key, - Package, - TrendingUp, - Building2, - ShieldCheck, - AlertTriangle, - List, - Home, - Menu, - X, - CheckCircle2, - Clock, - Info, -} from 'lucide-react'; -import { CodeBlock } from '@/components/docs/CodeBlock'; -import { DOC_SECTIONS, ALL_NAV_ITEMS } from '@/components/docs/docsNav'; - -// ─── Shared sub-components ──────────────────────────────────────────────────── - -function H2({ children }: { children: React.ReactNode }) { - return

{children}

; -} -function H3({ children }: { children: React.ReactNode }) { - return

{children}

; -} -function P({ children }: { children: React.ReactNode }) { - return

{children}

; -} -function Divider() { - return
; -} -function Callout({ type = 'info', children }: { type?: 'info' | 'warning' | 'success'; children: React.ReactNode }) { - const styles = { - info: 'bg-blue-50 border-blue-200 text-blue-800', - warning: 'bg-amber-50 border-amber-200 text-amber-800', - success: 'bg-green-50 border-green-200 text-green-800', - }; - const icons = { info: Info, warning: AlertTriangle, success: CheckCircle2 }; - const Icon = icons[type]; - return ( -
- -
{children}
-
- ); -} -function InlineCode({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} -function Table({ headers, rows }: { headers: string[]; rows: (string | React.ReactNode)[][] }) { - return ( -
- - - - {headers.map(h => ( - - ))} - - - - {rows.map((row, i) => ( - - {row.map((cell, j) => ( - - ))} - - ))} - -
- {h} -
- {cell} -
-
- ); -} -function SectionHeader({ title, description, badge }: { title: string; description: string; badge?: string }) { - return ( -
- {badge && ( - - {badge} - - )} -

{title}

-

{description}

-
- ); -} - -// ─── Section: Home ──────────────────────────────────────────────────────────── - -function HomeSection({ onNavigate }: { onNavigate: (id: string) => void }) { - const cards = [ - { - id: 'quickstart', - icon: Zap, - title: 'Guide de démarrage', - description: 'Faites votre première requête API en moins de 5 minutes.', - color: 'text-amber-500', - bg: 'bg-amber-50', - }, - { - id: 'authentication', - icon: Key, - title: 'Authentification', - description: 'Créez et gérez vos clés API pour accéder à la plateforme.', - color: 'text-blue-500', - bg: 'bg-blue-50', - }, - { - id: 'bookings', - icon: Package, - title: 'Bookings', - description: 'Créez, consultez et gérez des réservations de fret maritime.', - color: 'text-violet-500', - bg: 'bg-violet-50', - }, - { - id: 'rates', - icon: TrendingUp, - title: 'Tarifs & Recherche', - description: 'Recherchez et comparez des tarifs en temps réel.', - color: 'text-emerald-500', - bg: 'bg-emerald-50', - }, - { - id: 'organizations', - icon: Building2, - title: 'Organisations', - description: 'Accédez aux données de votre organisation.', - color: 'text-orange-500', - bg: 'bg-orange-50', - }, - { - id: 'endpoints', - icon: List, - title: 'Référence complète', - description: 'Tous les endpoints, paramètres et réponses.', - color: 'text-gray-500', - bg: 'bg-gray-50', - }, - ]; - - return ( -
- {/* Hero */} -
-
-
-
- - Plans Gold & Platinium uniquement -
-

- API Xpeditis -

-

- Intégrez la puissance de la freight logistics maritime dans vos applications. - Tarifs en temps réel, gestion de bookings, suivi d'expéditions. -

-
- - -
-
-
- - {/* Cards grid */} -
-

Explorer la documentation

-
- {cards.map(card => ( - - ))} -
-
- - {/* Base URL */} - -

URL de base

-

Toutes les requêtes API doivent être envoyées à l'URL de base suivante :

- -

En environnement de développement local :

- -
- ); -} - -// ─── Section: Quick Start ───────────────────────────────────────────────────── - -function QuickStartSection({ onNavigate }: { onNavigate: (id: string) => void }) { - return ( -
- - - - L'accès API est disponible uniquement sur les plans Gold et Platinium. - Rendez-vous dans Paramètres → Abonnement pour upgrader. - - - {/* Step 1 */} -
-
1
-
-

Obtenir votre clé API

-

- Dans le dashboard, rendez-vous dans Paramètres → Clés API, puis cliquez sur Créer une clé. - La clé complète vous sera montrée une seule fois — conservez-la immédiatement. -

-

Format de la clé :

- -
-
- - {/* Step 2 */} -
-
2
-
-

Faire votre première requête

-

Passez la clé dans l'en-tête X-API-Key :

- - -
-
- - {/* Step 3 */} -
-
3
-
-

Lire la réponse

-

Toutes les réponses sont en JSON. En cas de succès :

- -

En cas d'erreur :

- -
-
- - -

Étapes suivantes

-
- {[ - { id: 'authentication', label: 'Gérer vos clés API', desc: 'Créer, lister et révoquer des clés' }, - { id: 'bookings', label: 'Créer un booking', desc: 'Réservez du fret maritime via l\'API' }, - { id: 'rates', label: 'Rechercher des tarifs', desc: 'Comparez les tarifs en temps réel' }, - { id: 'errors', label: 'Gestion des erreurs', desc: 'Tous les codes d\'erreur expliqués' }, - ].map(item => ( - - ))} -
-
- ); -} - -// ─── Section: Authentication ────────────────────────────────────────────────── - -function AuthenticationSection() { - return ( -
- - -

Vue d'ensemble

-

- L'API Xpeditis utilise des clés API pour authentifier les requêtes. - Transmettez votre clé dans l'en-tête X-API-Key de chaque requête. -

- - Vos clés API sont confidentielles. Ne les partagez jamais dans du code public, des dépôts Git ou des forums. - En cas de compromission, révoquez immédiatement la clé depuis le dashboard. - - - -

Format de la clé

-

Toutes les clés Xpeditis commencent par xped_live_ suivi de 64 caractères hexadécimaux :

- - - -

Utilisation

-

Header HTTP

-

Passez votre clé dans l'en-tête X-API-Key :

- - -

Exemples par langage

- - - - - -

Endpoints de gestion

-

Ces endpoints nécessitent une authentification par token JWT (connexion via le dashboard) et non une clé API.

- - - -

Créer une clé

- " \\ - -H "Content-Type: application/json" \\ - -d '{ - "name": "Intégration ERP Production", - "expiresAt": "2027-01-01T00:00:00.000Z" - }'`} - /> - - - Le champ fullKey est retourné une seule fois. - Stockez-le immédiatement dans un gestionnaire de secrets. - - - -

Sécurité

-

Stockage recommandé

-
-

Rotation des clés

-

- Effectuez une rotation régulière (tous les 90 jours recommandé) : créez une nouvelle clé, migrez votre système, - puis révoquez l'ancienne. -

- - ); -} - -// ─── Section: Bookings ──────────────────────────────────────────────────────── - -function BookingsSection() { - return ( -
- - -

Lister les bookings

- -
status, 'string', 'Filtrer par statut (draft, confirmed, in_transit, delivered, cancelled)'], - [page, 'number', 'Numéro de page (défaut: 1)'], - [limit, 'number', 'Résultats par page (défaut: 20, max: 100)'], - [origin, 'string', 'Code port d\'origine (ex: FRLEH)'], - [destination, 'string', 'Code port de destination (ex: CNSHA)'], - ]} - /> - - - -

Créer un booking

- - - - -

Statuts d'un booking

-
draft, 'Réservation créée, non confirmée'], - [pending_confirmation, 'En attente de confirmation du transporteur'], - [confirmed, 'Confirmée par le transporteur'], - [in_transit, 'Expédition en cours'], - [delivered, 'Livraison confirmée'], - [cancelled, 'Annulée'], - ]} - /> - - ); -} - -// ─── Section: Rates ─────────────────────────────────────────────────────────── - -function RatesSection() { - return ( -
- - -

Rechercher des tarifs

- -
origin, '✅', 'Code port d\'origine (UN/LOCODE, ex: FRLEH)'], - [destination, '✅', 'Code port de destination (ex: CNSHA)'], - [containerType, '✅', 'Type de conteneur: 20GP, 40GP, 40HC, 45HC, 20FR, 40FR'], - [departureDate, '❌', 'Date souhaitée de départ (YYYY-MM-DD)'], - [sortBy, '❌', 'Tri: price_asc | price_desc | transit_time'], - ]} - /> - - - - Les tarifs sont mis en cache pendant 15 minutes. Au-delà, une nouvelle recherche est effectuée - auprès des transporteurs en temps réel. - - - -

Codes de ports (UN/LOCODE)

-

Les ports sont identifiés par le code standard UN/LOCODE (5 caractères).

-
- - - ); -} - -// ─── Section: Organizations ─────────────────────────────────────────────────── - -function OrganizationsSection() { - return ( -
- - -

Profil de l'organisation

- - - - -

Membres de l'organisation

- -
- ); -} - -// ─── Section: Endpoints ─────────────────────────────────────────────────────── - -function EndpointsSection() { - const endpoints = [ - { method: 'GET', path: '/bookings', desc: 'Lister les bookings' }, - { method: 'POST', path: '/bookings', desc: 'Créer un booking' }, - { method: 'GET', path: '/bookings/:id', desc: 'Détail d\'un booking' }, - { method: 'PATCH', path: '/bookings/:id/status', desc: 'Mettre à jour le statut' }, - { method: 'GET', path: '/rates/search', desc: 'Rechercher des tarifs' }, - { method: 'GET', path: '/rates/:id', desc: 'Détail d\'un tarif' }, - { method: 'GET', path: '/ports', desc: 'Lister les ports' }, - { method: 'GET', path: '/organizations/me', desc: 'Profil de l\'organisation' }, - { method: 'GET', path: '/users', desc: 'Membres de l\'organisation' }, - { method: 'POST', path: '/api-keys', desc: 'Créer une clé API (JWT requis)' }, - { method: 'GET', path: '/api-keys', desc: 'Lister les clés API (JWT requis)' }, - { method: 'DELETE', path: '/api-keys/:id', desc: 'Révoquer une clé (JWT requis)' }, - ]; - - const methodColor: Record = { - GET: 'text-emerald-700 bg-emerald-50 border-emerald-200', - POST: 'text-blue-700 bg-blue-50 border-blue-200', - PATCH: 'text-amber-700 bg-amber-50 border-amber-200', - DELETE: 'text-red-700 bg-red-50 border-red-200', - }; - - return ( -
- - -

Endpoints disponibles

-
- {endpoints.map((ep, i) => ( -
- - {ep.method} - - {ep.path} - {ep.desc} -
- ))} -
- - -

Format de réponse standard

- -
- ); -} - -// ─── Section: Errors ───────────────────────────────────────────────────────── - -function ErrorsSection() { - return ( -
- - -

Format d'erreur

-

Toutes les erreurs retournent un JSON standardisé :

- - - -

Codes HTTP

-
- - -

Erreurs courantes

-

401 — Clé API invalide

- -

Solutions : Vérifiez que la clé commence par xped_live_, qu'elle n'est pas révoquée et que l'abonnement est toujours Gold ou Platinium.

- -

403 — Plan insuffisant

- - -

429 — Rate limit

- -

En cas de 429, attendez la durée indiquée dans retryAfter avant de réessayer.

- - ); -} - -// ─── Section: Rate Limiting ─────────────────────────────────────────────────── - -function RateLimitingSection() { - return ( -
- - -

Limites par plan

-
- - Le rate limiting est appliqué par utilisateur (ID de l'utilisateur associé à la clé API). - - - -

En-têtes de réponse

-

Chaque réponse inclut des en-têtes pour suivre votre consommation :

-
- - -

Bonnes pratiques

-
- {[ - { icon: Clock, title: 'Exponential backoff', desc: 'En cas de 429, attendez avant de réessayer (1s, 2s, 4s, 8s…).' }, - { icon: CheckCircle2, title: 'Mise en cache', desc: 'Cachez les résultats côté client pour éviter les appels redondants. Les tarifs restent valides 15 minutes.' }, - { icon: ShieldCheck, title: 'Une clé par service', desc: 'Utilisez des clés séparées par service ou environnement pour un meilleur suivi et une révocation ciblée.' }, - ].map((item, i) => ( -
- -
-

{item.title}

-

{item.desc}

-
-
- ))} -
- - ); -} - -// ─── Section map ────────────────────────────────────────────────────────────── - -function SectionContent({ - activeSection, - onNavigate, -}: { - activeSection: string; - onNavigate: (id: string) => void; -}) { - switch (activeSection) { - case 'home': return ; - case 'quickstart': return ; - case 'authentication': return ; - case 'bookings': return ; - case 'rates': return ; - case 'organizations': return ; - case 'endpoints': return ; - case 'errors': return ; - case 'rate-limiting': return ; - default: return ; - } -} - -// ─── Main Page ──────────────────────────────────────────────────────────────── - -function DocsPageContent() { - const searchParams = useSearchParams(); - const router = useRouter(); - const [activeSection, setActiveSection] = useState(searchParams.get('section') ?? 'home'); - const [searchQuery, setSearchQuery] = useState(''); - const [sidebarOpen, setSidebarOpen] = useState(false); - - const navigate = (id: string) => { - setActiveSection(id); - router.replace(`/dashboard/docs?section=${id}`, { scroll: false }); - setSidebarOpen(false); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }; - - // Filter nav based on search - const filteredSections = DOC_SECTIONS.map(section => ({ - ...section, - items: section.items.filter(item => - item.label.toLowerCase().includes(searchQuery.toLowerCase()) - ), - })).filter(s => s.items.length > 0); - - const Sidebar = () => ( -
- {/* Logo + title */} -
-
-
- X -
- Xpeditis - / API -
- {/* Search */} -
- - setSearchQuery(e.target.value)} - className="w-full pl-8 pr-3 py-1.5 text-sm bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#34CCCD]/30 focus:border-[#34CCCD] placeholder-gray-400" - /> -
-
- - {/* Navigation */} - - - {/* Footer */} - -
- ); - - return ( - /* Break out of dashboard's p-6 padding */ -
- {/* Mobile sidebar overlay */} - {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} - - {/* Docs sidebar — desktop */} -
- -
- - {/* Docs sidebar — mobile drawer */} -
- -
- - {/* Main content */} -
- {/* Mobile top bar */} -
- - - {ALL_NAV_ITEMS.find(i => i.id === activeSection)?.label ?? 'Documentation'} - -
- -
- {/* Breadcrumb */} -
- - {activeSection !== 'home' && ( - <> - - - {ALL_NAV_ITEMS.find(i => i.id === activeSection)?.label} - - - )} -
- - - - {/* Bottom nav */} - {activeSection !== 'home' && ( -
- -
- )} -
-
-
- ); -} +import { DocsPageContent } from '@/components/docs/DocsPageContent'; export default function DocsPage() { - return ( -
}> - -
- ); + return ; } diff --git a/apps/frontend/app/dashboard/layout.tsx b/apps/frontend/app/dashboard/layout.tsx index 8f785b9..1510854 100644 --- a/apps/frontend/app/dashboard/layout.tsx +++ b/apps/frontend/app/dashboard/layout.tsx @@ -23,7 +23,6 @@ import { Users, LogOut, Lock, - Code2, Key, } from 'lucide-react'; import { useSubscription } from '@/lib/context/subscription-context'; @@ -62,7 +61,6 @@ export default function DashboardLayout({ children }: { children: React.ReactNod { name: 'Suivi', href: '/dashboard/track-trace', icon: Search, requiredFeature: 'dashboard' }, { name: 'Wiki Maritime', href: '/dashboard/wiki', icon: BookOpen, requiredFeature: 'wiki' }, { name: 'Organisation', href: '/dashboard/settings/organization', icon: Building2 }, - { name: 'Documentation API', href: '/dashboard/docs', icon: Code2 }, { name: 'Clés API', href: '/dashboard/settings/api-keys', icon: Key, requiredFeature: 'api_access' as PlanFeature }, // ADMIN and MANAGER only navigation items ...(user?.role === 'ADMIN' || user?.role === 'MANAGER' ? [ diff --git a/apps/frontend/app/docs/api/page.tsx b/apps/frontend/app/docs/api/page.tsx new file mode 100644 index 0000000..9223dc1 --- /dev/null +++ b/apps/frontend/app/docs/api/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { DocsPageContent } from '@/components/docs/DocsPageContent'; + +export default function PublicDocsPage() { + return ; +} diff --git a/apps/frontend/app/docs/layout.tsx b/apps/frontend/app/docs/layout.tsx new file mode 100644 index 0000000..8c61ae4 --- /dev/null +++ b/apps/frontend/app/docs/layout.tsx @@ -0,0 +1,16 @@ +import { LandingHeader } from '@/components/layout/LandingHeader'; +import { LandingFooter } from '@/components/layout/LandingFooter'; + +export const metadata = { + title: 'Documentation API — Xpeditis', + description: 'Documentation de l\'API Xpeditis pour intégrer le fret maritime dans vos applications.', +}; + +export default function DocsLayout({ children }: { children: React.ReactNode }) { + return ( + <> + +
{children}
+ + ); +} diff --git a/apps/frontend/app/page.tsx b/apps/frontend/app/page.tsx index d5f2af3..f2b319f 100644 --- a/apps/frontend/app/page.tsx +++ b/apps/frontend/app/page.tsx @@ -70,7 +70,7 @@ export default function LandingPage() { const heroRef = useRef(null); const featuresRef = useRef(null); const statsRef = useRef(null); - const toolsRef = useRef(null); + const pricingRef = useRef(null); const testimonialsRef = useRef(null); const ctaRef = useRef(null); @@ -79,7 +79,7 @@ export default function LandingPage() { const isHeroInView = useInView(heroRef, { once: true }); const isFeaturesInView = useInView(featuresRef, { once: true }); const isStatsInView = useInView(statsRef, { once: true, amount: 0.3 }); - const isToolsInView = useInView(toolsRef, { once: true }); + const isPricingInView = useInView(pricingRef, { once: true }); const isTestimonialsInView = useInView(testimonialsRef, { once: true }); const isCtaInView = useInView(ctaRef, { once: true }); @@ -139,44 +139,6 @@ export default function LandingPage() { }, ]; - const tools = [ - { - icon: LayoutDashboard, - title: 'Tableau de bord', - description: 'Vue d\'ensemble de votre activité maritime', - link: '/dashboard', - }, - { - icon: Package, - title: 'Mes Réservations', - description: 'Gérez toutes vos réservations en un seul endroit', - link: '/dashboard/bookings', - }, - { - icon: FileText, - title: 'Documents', - description: 'Accédez à tous vos documents maritimes', - link: '/dashboard/documents', - }, - { - icon: Search, - title: 'Suivi des expéditions', - description: 'Suivez vos conteneurs en temps réel', - link: '/dashboard/track-trace', - }, - { - icon: BookOpen, - title: 'Wiki Maritime', - description: 'Base de connaissances du fret maritime', - link: '/dashboard/wiki', - }, - { - icon: Users, - title: 'Mon Profil', - description: 'Gérez vos informations personnelles', - link: '/dashboard/profile', - }, - ]; const stats = [ { end: 50, prefix: '', suffix: '+', decimals: 0, label: 'Compagnies Maritimes', icon: Ship }, @@ -237,7 +199,7 @@ export default function LandingPage() { { text: 'Accès API', included: false }, { text: 'KAM dédié', included: false }, ], - cta: 'Essai gratuit 14 jours', + cta: 'Commencer', ctaLink: '/register', highlighted: true, accentColor: 'from-slate-400 to-slate-500', @@ -266,7 +228,7 @@ export default function LandingPage() { { text: 'Accès API complet', included: true }, { text: 'KAM dédié', included: false }, ], - cta: 'Essai gratuit 14 jours', + cta: 'Commencer', ctaLink: '/register', highlighted: false, accentColor: 'from-yellow-400 to-amber-400', @@ -600,67 +562,6 @@ export default function LandingPage() {
- {/* Tools & Calculators Section */} -
-
- -

- Outils & Calculateurs -

-

- Des outils puissants pour optimiser vos opérations maritimes -

-
- - - {tools.map((tool, index) => { - const IconComponent = tool.icon; - return ( - - -
-
- -
-
-

- {tool.title} -

-

{tool.description}

-
- -
- -
- ); - })} -
-
-
{/* Partner Logos Section */}
@@ -928,7 +829,7 @@ export default function LandingPage() { className="mt-12 text-center space-y-2" >

- Plans Silver et Gold : essai gratuit 14 jours inclus · Aucune carte bancaire requise + Sans engagement · Résiliable à tout moment

Des questions ?{' '} diff --git a/apps/frontend/middleware.ts b/apps/frontend/middleware.ts index efe74ef..5a7fba3 100644 --- a/apps/frontend/middleware.ts +++ b/apps/frontend/middleware.ts @@ -24,6 +24,7 @@ const prefixPublicPaths = [ '/contact', '/carrier', '/pricing', + '/docs', ]; export function middleware(request: NextRequest) { diff --git a/apps/frontend/src/components/docs/DocsPageContent.tsx b/apps/frontend/src/components/docs/DocsPageContent.tsx new file mode 100644 index 0000000..9f77475 --- /dev/null +++ b/apps/frontend/src/components/docs/DocsPageContent.tsx @@ -0,0 +1,1190 @@ +'use client'; + +import { useState, Suspense } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { + Search, + ChevronRight, + ArrowRight, + Key, + Package, + TrendingUp, + Building2, + ShieldCheck, + AlertTriangle, + List, + Menu, + X, + CheckCircle2, + Clock, + Info, + Zap, + Home, + ChevronLeft, + Circle, +} from 'lucide-react'; +import { CodeBlock } from '@/components/docs/CodeBlock'; +import { DOC_SECTIONS, ALL_NAV_ITEMS } from '@/components/docs/docsNav'; + +// ─── Design tokens ──────────────────────────────────────────────────────────── +const BRAND = { + navy: '#10183A', + turquoise: '#34CCCD', + teal: '#0e9999', +}; + +// ─── Reusable primitives ────────────────────────────────────────────────────── + +function H1({ children }: { children: React.ReactNode }) { + return

{children}

; +} +function H2({ children }: { children: React.ReactNode }) { + return ( +

+ + {children} +

+ ); +} +function H3({ children }: { children: React.ReactNode }) { + return

{children}

; +} +function P({ children }: { children: React.ReactNode }) { + return

{children}

; +} +function Divider() { + return
; +} +function InlineCode({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +function Callout({ type = 'info', children }: { type?: 'info' | 'warning' | 'success'; children: React.ReactNode }) { + const styles = { + info: { border: 'border-blue-200', bg: 'bg-blue-50', text: 'text-blue-800', icon: Info, dot: 'bg-blue-400' }, + warning: { border: 'border-amber-200', bg: 'bg-amber-50', text: 'text-amber-800', icon: AlertTriangle, dot: 'bg-amber-400' }, + success: { border: 'border-emerald-200', bg: 'bg-emerald-50', text: 'text-emerald-800', icon: CheckCircle2, dot: 'bg-emerald-400' }, + }; + const s = styles[type]; + const Icon = s.icon; + return ( +
+ +
{children}
+
+ ); +} + +function HttpBadge({ method }: { method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT' }) { + const styles: Record = { + GET: 'bg-emerald-100 text-emerald-700 border-emerald-200', + POST: 'bg-blue-100 text-blue-700 border-blue-200', + PATCH: 'bg-amber-100 text-amber-700 border-amber-200', + PUT: 'bg-orange-100 text-orange-700 border-orange-200', + DELETE: 'bg-red-100 text-red-700 border-red-200', + }; + return ( + + {method} + + ); +} + +function EndpointRow({ method, path, desc }: { method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT'; path: string; desc: string }) { + return ( +
+ + {path} + {desc} +
+ ); +} + +function ParamTable({ headers, rows }: { headers: string[]; rows: (string | React.ReactNode)[][] }) { + return ( +
+
+ + + {headers.map(h => ( + + ))} + + + + {rows.map((row, i) => ( + + {row.map((cell, j) => ( + + ))} + + ))} + +
+ {h} +
+ {cell} +
+
+ ); +} + +function SectionHeader({ title, description, badge }: { title: string; description: string; badge?: string }) { + return ( +
+ {badge && ( + + {badge} + + )} +

{title}

+

{description}

+
+ ); +} + +function RequiredBadge({ required }: { required: boolean }) { + return required + ? requis + : optionnel; +} + +function LabeledBlock({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+

{label}

+ {children} +
+ ); +} + +// ─── Section: Home ──────────────────────────────────────────────────────────── + +function HomeSection({ onNavigate }: { onNavigate: (id: string) => void }) { + const quickLinks = [ + { id: 'quickstart', icon: Zap, label: 'Démarrage rapide', desc: 'Première requête en 5 min', color: 'text-amber-500', bg: 'bg-amber-50', border: 'border-amber-100' }, + { id: 'authentication', icon: Key, label: 'Authentification', desc: 'Créer et gérer vos clés API', color: 'text-blue-500', bg: 'bg-blue-50', border: 'border-blue-100' }, + { id: 'bookings', icon: Package, label: 'Bookings', desc: 'Réservations de fret maritime', color: 'text-violet-500', bg: 'bg-violet-50', border: 'border-violet-100' }, + { id: 'rates', icon: TrendingUp,label: 'Tarifs', desc: 'Tarifs en temps réel', color: 'text-emerald-500',bg: 'bg-emerald-50',border: 'border-emerald-100' }, + { id: 'endpoints', icon: List, label: 'Référence complète', desc: 'Tous les endpoints', color: 'text-gray-500', bg: 'bg-gray-50', border: 'border-gray-200' }, + { id: 'errors', icon: AlertTriangle, label: 'Erreurs', desc: 'Codes et gestion des erreurs', color: 'text-red-400', bg: 'bg-red-50', border: 'border-red-100' }, + ]; + + return ( +
+ {/* Hero */} +
+
+ {/* Grid pattern */} +
+
+
+
+ API v1.0 · Plans Gold & Platinium +
+

+ API Xpeditis +

+

+ Intégrez le fret maritime dans vos applications. Tarifs en temps réel, + gestion de bookings et suivi d'expéditions via une API REST simple. +

+ +
+ + + + Gérer mes clés API + +
+ + {/* Base URL */} +
+ BASE URL + https://api.xpeditis.com +
+
+
+ + {/* Quick links grid */} +
+

Explorer la documentation

+
+ {quickLinks.map(item => ( + + ))} +
+
+ + + + {/* How it works */} +

Fonctionnement

+
+ {[ + { step: '01', title: 'Authentification par clé API', desc: <>Passez votre clé dans l'en-tête X-API-Key de chaque requête. }, + { step: '02', title: 'Format JSON standard', desc: 'Toutes les réponses sont en JSON avec une enveloppe data/meta cohérente.' }, + { step: '03', title: 'Rate limiting transparent', desc: 'Les quotas sont exposés dans les en-têtes X-RateLimit-* de chaque réponse.' }, + ].map(item => ( +
+ {item.step} +
+

{item.title}

+

{item.desc}

+
+
+ ))} +
+
+ ); +} + +// ─── Section: Quick Start ───────────────────────────────────────────────────── + +function QuickStartSection({ onNavigate }: { onNavigate: (id: string) => void }) { + return ( +
+ + + + L'accès API est réservé aux plans Gold et Platinium. + Accédez à Paramètres → Abonnement pour upgrader. + + + {/* Steps */} + {[ + { + n: 1, + title: 'Obtenir votre clé API', + content: ( + <> +

Dans le dashboard, allez dans Paramètres → Clés API, cliquez Créer une clé. La clé complète est affichée une seule fois — copiez-la immédiatement.

+ + + ), + }, + { + n: 2, + title: 'Envoyer votre première requête', + content: ( + <> +

Passez la clé dans l'en-tête X-API-Key :

+ + + + ), + }, + { + n: 3, + title: 'Lire la réponse', + content: ( + <> +

Toutes les réponses suivent le même format :

+ + + + + + + + ), + }, + ].map(step => ( +
+
+
{step.n}
+ {step.n < 3 &&
} +
+
+

{step.title}

+ {step.content} +
+
+ ))} + + +

Étapes suivantes

+
+ {[ + { id: 'authentication', label: 'Gérer vos clés API', desc: 'Créer, lister et révoquer' }, + { id: 'bookings', label: 'Créer un booking', desc: 'Réservez du fret maritime' }, + { id: 'rates', label: 'Rechercher des tarifs', desc: 'Comparez en temps réel' }, + { id: 'errors', label: 'Gestion des erreurs', desc: 'Tous les codes expliqués' }, + ].map(item => ( + + ))} +
+
+ ); +} + +// ─── Section: Authentication ────────────────────────────────────────────────── + +function AuthenticationSection() { + return ( +
+ + + + Vos clés sont confidentielles. Ne les exposez jamais dans du code public. + En cas de compromission, révoquez immédiatement depuis le dashboard. + + +

Format

+

Toutes les clés Xpeditis commencent par xped_live_ suivi de 64 caractères hexadécimaux :

+ + + +

Utilisation

+

Passez votre clé dans l'en-tête X-API-Key de chaque requête :

+ + + +

Exemples par langage

+ + + + + +

Gestion des clés

+

Ces endpoints nécessitent un token JWT (connexion via le dashboard), pas une clé API.

+ +
+ {[ + { method: 'POST' as const, path: '/api-keys', desc: 'Créer une nouvelle clé' }, + { method: 'GET' as const, path: '/api-keys', desc: 'Lister toutes les clés' }, + { method: 'DELETE' as const, path: '/api-keys/:id', desc: 'Révoquer une clé' }, + ].map((ep, i) => )} +
+ +

Créer une clé

+ + " \\ + -H "Content-Type: application/json" \\ + -d '{"name": "Intégration ERP", "expiresAt": "2027-01-01T00:00:00.000Z"}'`} + /> + + + + + + +

Sécurité & rotation

+ + + Effectuez une rotation tous les 90 jours : créez une nouvelle clé, migrez votre système, puis révoquez l'ancienne. + +
+ ); +} + +// ─── Section: Bookings ──────────────────────────────────────────────────────── + +function BookingsSection() { + return ( +
+ + +

Lister les bookings

+
+ +
+ + status, 'string', , 'draft | confirmed | in_transit | delivered | cancelled'], + [page, 'number', , 'Numéro de page (défaut : 1)'], + [limit, 'number', , 'Résultats par page (défaut : 20, max : 100)'], + [origin, 'string', , 'Code port UN/LOCODE (ex : FRLEH)'], + [destination, 'string', , 'Code port UN/LOCODE (ex : CNSHA)'], + ]} + /> + + + + + + +

Créer un booking

+
+ +
+ + + + + + +

Statuts d'un booking

+ draft, 'Créé, non confirmé'], + [pending_confirmation, 'En attente du transporteur'], + [confirmed, 'Confirmé par le transporteur'], + [in_transit, 'Expédition en cours'], + [delivered, 'Livraison confirmée'], + [cancelled, 'Annulé'], + ]} + /> +
+ ); +} + +// ─── Section: Rates ─────────────────────────────────────────────────────────── + +function RatesSection() { + return ( +
+ + +

Rechercher des tarifs

+
+ +
+ + origin, , 'Code port origine (UN/LOCODE, ex : FRLEH)'], + [destination, , 'Code port destination (ex : CNSHA)'], + [containerType, , '20GP · 40GP · 40HC · 45HC · 20FR · 40FR'], + [departureDate, , 'Date souhaitée (YYYY-MM-DD)'], + [sortBy, , 'price_asc · price_desc · transit_time'], + ]} + /> + + + + + + + Les tarifs sont mis en cache 15 minutes. Après expiration, une nouvelle requête est envoyée aux transporteurs en temps réel. + + + +

Codes de ports (UN/LOCODE)

+
+ +
+ + + +
+ ); +} + +// ─── Section: Organizations ─────────────────────────────────────────────────── + +function OrganizationsSection() { + return ( +
+ + +

Profil de l'organisation

+
+ +
+ + + + + + +

Membres

+
+ +
+ +
+ ); +} + +// ─── Section: Endpoints ─────────────────────────────────────────────────────── + +function EndpointsSection() { + const groups: { label: string; endpoints: { method: 'GET' | 'POST' | 'PATCH' | 'DELETE'; path: string; desc: string }[] }[] = [ + { + label: 'Bookings', + endpoints: [ + { method: 'GET', path: '/bookings', desc: 'Lister les bookings' }, + { method: 'POST', path: '/bookings', desc: 'Créer un booking' }, + { method: 'GET', path: '/bookings/:id', desc: 'Détail d\'un booking' }, + { method: 'PATCH', path: '/bookings/:id/status',desc: 'Mettre à jour le statut' }, + ], + }, + { + label: 'Tarifs', + endpoints: [ + { method: 'GET', path: '/rates/search', desc: 'Rechercher des tarifs' }, + { method: 'GET', path: '/rates/:id', desc: 'Détail d\'un tarif' }, + { method: 'GET', path: '/ports', desc: 'Lister les ports' }, + ], + }, + { + label: 'Organisation', + endpoints: [ + { method: 'GET', path: '/organizations/me', desc: 'Profil de l\'organisation' }, + { method: 'GET', path: '/users', desc: 'Membres de l\'organisation' }, + ], + }, + { + label: 'Clés API (JWT requis)', + endpoints: [ + { method: 'POST', path: '/api-keys', desc: 'Créer une clé API' }, + { method: 'GET', path: '/api-keys', desc: 'Lister les clés API' }, + { method: 'DELETE', path: '/api-keys/:id', desc: 'Révoquer une clé' }, + ], + }, + ]; + + return ( +
+ + + {groups.map(group => ( +
+

{group.label}

+
+ {group.endpoints.map((ep, i) => )} +
+
+ ))} + + +

Format de réponse standard

+ + + + + + +
+ ); +} + +// ─── Section: Errors ───────────────────────────────────────────────────────── + +function ErrorsSection() { + return ( +
+ + + + + +

Format des erreurs

+ + +

Exemples

+ + + + + + + + En cas de 429, respectez le délai retryAfter (en secondes) avec un backoff exponentiel. + +
+ ); +} + +// ─── Section: Rate Limiting ─────────────────────────────────────────────────── + +function RateLimitingSection() { + return ( +
+ + +

Limites par plan

+ + + Le rate limiting est calculé par utilisateur associé à la clé API. + + + +

En-têtes de réponse

+ X-RateLimit-Limit, 'Nombre maximum de requêtes par fenêtre'], + [X-RateLimit-Remaining, 'Requêtes restantes dans la fenêtre'], + [X-RateLimit-Reset, 'Timestamp UNIX de réinitialisation'], + ]} + /> + + +

Bonnes pratiques

+
+ {[ + { icon: Clock, title: 'Backoff exponentiel', desc: 'En cas de 429, attendez 1s, 2s, 4s, 8s… avant de réessayer.' }, + { icon: CheckCircle2, title: 'Mise en cache', desc: 'Cachez les tarifs côté client — ils sont valides 15 minutes.' }, + { icon: ShieldCheck, title: 'Une clé par service', desc: 'Clés séparées par service/env pour un suivi précis et une révocation ciblée.' }, + ].map((item, i) => ( +
+
+ +
+
+

{item.title}

+

{item.desc}

+
+
+ ))} +
+
+ ); +} + +// ─── Section map ────────────────────────────────────────────────────────────── + +function SectionContent({ activeSection, onNavigate }: { activeSection: string; onNavigate: (id: string) => void }) { + switch (activeSection) { + case 'home': return ; + case 'quickstart': return ; + case 'authentication': return ; + case 'bookings': return ; + case 'rates': return ; + case 'organizations':return ; + case 'endpoints': return ; + case 'errors': return ; + case 'rate-limiting':return ; + default: return ; + } +} + +// ─── Prev / Next ────────────────────────────────────────────────────────────── + +function PrevNext({ activeSection, onNavigate }: { activeSection: string; onNavigate: (id: string) => void }) { + const idx = ALL_NAV_ITEMS.findIndex(i => i.id === activeSection); + const prev = idx > 0 ? ALL_NAV_ITEMS[idx - 1] : null; + const next = idx < ALL_NAV_ITEMS.length - 1 ? ALL_NAV_ITEMS[idx + 1] : null; + + if (!prev && !next) return null; + + return ( +
+ {prev ? ( + + ) :
} + + {next ? ( + + ) :
} +
+ ); +} + +// ─── Main Component ─────────────────────────────────────────────────────────── + +export interface DocsPageContentProps { + basePath: string; + /** 'dashboard' = fixed height layout, 'public' = scrollable with sticky sidebar */ + variant?: 'dashboard' | 'public'; +} + +function DocsPageInner({ basePath, variant = 'public' }: DocsPageContentProps) { + const searchParams = useSearchParams(); + const router = useRouter(); + const [activeSection, setActiveSection] = useState(searchParams.get('section') ?? 'home'); + const [searchQuery, setSearchQuery] = useState(''); + const [sidebarOpen, setSidebarOpen] = useState(false); + + const navigate = (id: string) => { + setActiveSection(id); + router.replace(`${basePath}?section=${id}`, { scroll: false }); + setSidebarOpen(false); + if (variant === 'public') { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }; + + const filteredSections = DOC_SECTIONS.map(section => ({ + ...section, + items: section.items.filter(item => + item.label.toLowerCase().includes(searchQuery.toLowerCase()) + ), + })).filter(s => s.items.length > 0); + + const stickyTop = variant === 'dashboard' ? 'top-16' : 'top-20'; + const sidebarHeight= variant === 'dashboard' ? 'calc(100vh - 64px)' : 'calc(100vh - 80px)'; + + // Sidebar inner content + const SidebarInner = () => ( +
+ {/* Brand + search */} +
+
+
+ X +
+ Xpeditis + v1.0 +
+ + {/* API status */} +
+ + Tous les services opérationnels +
+ + {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="w-full pl-8 pr-3 py-1.5 text-sm bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#34CCCD]/30 focus:border-[#34CCCD] placeholder-gray-400 transition-all" + /> +
+
+ + {/* Navigation */} + + + {/* Footer CTA */} +
+ +
+ +
+ Gérer mes clés API + + +
+
+ ); + + // ── Dashboard variant: fixed-height layout ─────────────────────────────── + if (variant === 'dashboard') { + return ( +
+ {sidebarOpen && ( +
setSidebarOpen(false)} /> + )} + + {/* Desktop sidebar */} +
+ +
+ + {/* Mobile drawer */} +
+ +
+ + {/* Content */} +
+
+ + + {ALL_NAV_ITEMS.find(i => i.id === activeSection)?.label ?? 'Documentation'} + +
+
+ + + +
+
+
+ ); + } + + // ── Public variant: scrollable with sticky sidebar ─────────────────────── + return ( +
+ {/* Mobile overlay */} + {sidebarOpen && ( +
setSidebarOpen(false)} /> + )} + + {/* Mobile drawer */} +
+ +
+ + {/* Desktop sticky sidebar */} + + + {/* Main scrollable content */} +
+ {/* Mobile top bar */} +
+ + + {ALL_NAV_ITEMS.find(i => i.id === activeSection)?.label ?? 'Documentation'} + + {sidebarOpen && ( + + )} +
+ +
+ + + +
+
+
+ ); +} + +// ─── Breadcrumb ─────────────────────────────────────────────────────────────── + +function Breadcrumb({ activeSection, onNavigate }: { activeSection: string; onNavigate: (id: string) => void }) { + if (activeSection === 'home') return null; + const current = ALL_NAV_ITEMS.find(i => i.id === activeSection); + return ( + + ); +} + +// ─── Public export ──────────────────────────────────────────────────────────── + +export function DocsPageContent(props: DocsPageContentProps) { + const fallbackStyle = props.variant === 'dashboard' + ? { height: 'calc(100vh - 64px)' } + : { minHeight: 'calc(100vh - 80px)' }; + + return ( + +
+
+ }> + +
+ ); +} diff --git a/apps/frontend/src/components/layout/LandingHeader.tsx b/apps/frontend/src/components/layout/LandingHeader.tsx index f11307e..e225a22 100644 --- a/apps/frontend/src/components/layout/LandingHeader.tsx +++ b/apps/frontend/src/components/layout/LandingHeader.tsx @@ -9,12 +9,13 @@ import { Info, BookOpen, LayoutDashboard, + Code2, } from 'lucide-react'; import { useAuth } from '@/lib/context/auth-context'; interface LandingHeaderProps { transparentOnTop?: boolean; - activePage?: 'about' | 'contact' | 'careers' | 'blog' | 'press'; + activePage?: 'about' | 'contact' | 'careers' | 'blog' | 'press' | 'docs'; } export function LandingHeader({ transparentOnTop = false, activePage }: LandingHeaderProps) { @@ -91,12 +92,6 @@ export function LandingHeader({ transparentOnTop = false, activePage }: LandingH > Fonctionnalités - - Outils - - {/* Contact — lien direct dans la nav principale */} - - Contact - - {/* Menu Entreprise */}
+ + Contact + + + + + Docs API + + {/* Affichage conditionnel: connecté vs non connecté */} {loading ? (