fix documentation et landing page
This commit is contained in:
parent
0e4c0d7785
commit
e1f813bd92
@ -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
|
||||
|
||||
@ -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
@ -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' ? [
|
||||
|
||||
7
apps/frontend/app/docs/api/page.tsx
Normal file
7
apps/frontend/app/docs/api/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { DocsPageContent } from '@/components/docs/DocsPageContent';
|
||||
|
||||
export default function PublicDocsPage() {
|
||||
return <DocsPageContent basePath="/docs/api" variant="public" />;
|
||||
}
|
||||
16
apps/frontend/app/docs/layout.tsx
Normal file
16
apps/frontend/app/docs/layout.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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 ?{' '}
|
||||
|
||||
@ -24,6 +24,7 @@ const prefixPublicPaths = [
|
||||
'/contact',
|
||||
'/carrier',
|
||||
'/pricing',
|
||||
'/docs',
|
||||
];
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
|
||||
1190
apps/frontend/src/components/docs/DocsPageContent.tsx
Normal file
1190
apps/frontend/src/components/docs/DocsPageContent.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -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" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user