fix documentation et landing page

This commit is contained in:
David 2026-04-01 20:33:22 +02:00
parent 0e4c0d7785
commit e1f813bd92
10 changed files with 1318 additions and 1261 deletions

View File

@ -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

View File

@ -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<number>('PORT', 4000);
const apiPrefix = configService.get<string>('API_PREFIX', 'api/v1');
const isProduction = configService.get<string>('NODE_ENV') === 'production';
// Use Pino logger
app.useLogger(app.get(Logger));
@ -52,7 +54,35 @@ async function bootstrap() {
})
);
// Swagger documentation
// ─── Swagger documentation ────────────────────────────────────────────────
const swaggerUser = configService.get<string>('SWAGGER_USERNAME');
const swaggerPass = configService.get<string>('SWAGGER_PASSWORD');
const swaggerEnabled = !isProduction || (Boolean(swaggerUser) && Boolean(swaggerPass));
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(
@ -60,6 +90,7 @@ async function bootstrap() {
)
.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')
@ -73,18 +104,26 @@ async function bootstrap() {
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 ║
Docs: ${swaggerStatus}
`);
}

File diff suppressed because it is too large Load Diff

View File

@ -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' ? [

View File

@ -0,0 +1,7 @@
'use client';
import { DocsPageContent } from '@/components/docs/DocsPageContent';
export default function PublicDocsPage() {
return <DocsPageContent basePath="/docs/api" variant="public" />;
}

View File

@ -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 (
<>
<LandingHeader />
<main className="pt-20">{children}</main>
</>
);
}

View File

@ -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() {
</div>
</section>
{/* Tools & Calculators Section */}
<section
ref={toolsRef}
id="tools"
className="py-20 lg:py-32 bg-gradient-to-br from-gray-50 to-white"
>
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isToolsInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-4xl lg:text-5xl font-bold text-brand-navy mb-4">
Outils & Calculateurs
</h2>
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
Des outils puissants pour optimiser vos opérations maritimes
</p>
</motion.div>
<motion.div
variants={containerVariants}
initial="hidden"
animate={isToolsInView ? 'visible' : 'hidden'}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
>
{tools.map((tool, index) => {
const IconComponent = tool.icon;
return (
<motion.div
key={index}
variants={itemVariants}
whileHover={{ y: -5 }}
className="group"
>
<Link
href={tool.link}
target="_blank"
rel="noopener noreferrer"
className="block bg-white p-6 rounded-xl border-2 border-gray-200 hover:border-brand-turquoise transition-all hover:shadow-lg"
>
<div className="flex items-start space-x-4">
<div className="flex-shrink-0 w-12 h-12 bg-brand-turquoise/10 rounded-lg flex items-center justify-center group-hover:bg-brand-turquoise/20 transition-colors">
<IconComponent className="w-6 h-6 text-brand-turquoise" />
</div>
<div className="flex-1">
<h3 className="text-lg font-bold text-brand-navy mb-1 group-hover:text-brand-turquoise transition-colors">
{tool.title}
</h3>
<p className="text-sm text-gray-600">{tool.description}</p>
</div>
<ArrowRight className="w-5 h-5 text-gray-400 group-hover:text-brand-turquoise group-hover:translate-x-1 transition-all" />
</div>
</Link>
</motion.div>
);
})}
</motion.div>
</div>
</section>
{/* Partner Logos Section */}
<section className="py-16 bg-white">
@ -928,7 +829,7 @@ export default function LandingPage() {
className="mt-12 text-center space-y-2"
>
<p className="text-gray-600 text-sm">
Plans Silver et Gold : essai gratuit 14 jours inclus · Aucune carte bancaire requise
Sans engagement · Résiliable à tout moment
</p>
<p className="text-sm text-gray-500">
Des questions ?{' '}

View File

@ -24,6 +24,7 @@ const prefixPublicPaths = [
'/contact',
'/carrier',
'/pricing',
'/docs',
];
export function middleware(request: NextRequest) {

File diff suppressed because it is too large Load Diff

View File

@ -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
</Link>
<Link
href="/#tools"
className="text-white hover:text-brand-turquoise transition-colors font-medium"
>
Outils
</Link>
<Link
href="/#pricing"
className="text-white hover:text-brand-turquoise transition-colors font-medium"
@ -104,18 +99,6 @@ export function LandingHeader({ transparentOnTop = false, activePage }: LandingH
Tarifs
</Link>
{/* Contact — lien direct dans la nav principale */}
<Link
href="/contact"
className={`transition-colors font-medium ${
activePage === 'contact'
? 'text-brand-turquoise'
: 'text-white hover:text-brand-turquoise'
}`}
>
Contact
</Link>
{/* Menu Entreprise */}
<div
className="relative"
@ -184,6 +167,29 @@ export function LandingHeader({ transparentOnTop = false, activePage }: LandingH
</AnimatePresence>
</div>
<Link
href="/contact"
className={`transition-colors font-medium ${
activePage === 'contact'
? 'text-brand-turquoise'
: 'text-white hover:text-brand-turquoise'
}`}
>
Contact
</Link>
<Link
href="/docs/api"
className={`flex items-center gap-1.5 transition-colors font-medium ${
activePage === 'docs'
? 'text-brand-turquoise'
: 'text-white hover:text-brand-turquoise'
}`}
>
<Code2 className="w-4 h-4" />
Docs API
</Link>
{/* Affichage conditionnel: connecté vs non connecté */}
{loading ? (
<div className="w-8 h-8 rounded-full bg-white/20 animate-pulse" />