diff options
| author | ertopogo <erwin.t.pombett@gmail.com> | 2026-02-19 11:34:16 +0100 |
|---|---|---|
| committer | ertopogo <erwin.t.pombett@gmail.com> | 2026-02-19 11:34:16 +0100 |
| commit | a21bd6a6710d123ef3bfc3c9aab37fc0c276f9c5 (patch) | |
| tree | e2cc828607ea91e5c90ae0ea98c6b7d11324eaf1 /src | |
feat: initial project setup - Next.js 16, Payload CMS v3, palette Mapuche
Next.js 16 App Router + TypeScript + Tailwind CSS v4. Payload CMS v3 with PostgreSQL adapter. Mapuche Corporate palette. Public pages, Docker Compose + Caddy, security middleware.
Co-authored-by: Cursor <cursoragent@cursor.com>
Diffstat (limited to 'src')
24 files changed, 1408 insertions, 0 deletions
diff --git a/src/app/(payload)/admin/[[...segments]]/not-found.tsx b/src/app/(payload)/admin/[[...segments]]/not-found.tsx new file mode 100644 index 0000000..62ffef8 --- /dev/null +++ b/src/app/(payload)/admin/[[...segments]]/not-found.tsx @@ -0,0 +1,23 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import type { Metadata } from "next"; + +import config from "@payload-config"; +import { NotFoundPage, generatePageMetadata } from "@payloadcms/next/views"; +import { importMap } from "../importMap"; + +type Args = { + params: Promise<{ segments: string[] }>; + searchParams: Promise<{ [key: string]: string | string[] }>; +}; + +export const generateMetadata = ({ + params, + searchParams, +}: Args): Promise<Metadata> => + generatePageMetadata({ config, params, searchParams }); + +const NotFound = ({ params, searchParams }: Args) => + NotFoundPage({ config, importMap, params, searchParams }); + +export default NotFound; diff --git a/src/app/(payload)/admin/[[...segments]]/page.tsx b/src/app/(payload)/admin/[[...segments]]/page.tsx new file mode 100644 index 0000000..3132b87 --- /dev/null +++ b/src/app/(payload)/admin/[[...segments]]/page.tsx @@ -0,0 +1,23 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import type { Metadata } from "next"; + +import config from "@payload-config"; +import { RootPage, generatePageMetadata } from "@payloadcms/next/views"; +import { importMap } from "../importMap"; + +type Args = { + params: Promise<{ segments: string[] }>; + searchParams: Promise<{ [key: string]: string | string[] }>; +}; + +export const generateMetadata = ({ + params, + searchParams, +}: Args): Promise<Metadata> => + generatePageMetadata({ config, params, searchParams }); + +const Page = ({ params, searchParams }: Args) => + RootPage({ config, params, searchParams, importMap }); + +export default Page; diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js new file mode 100644 index 0000000..6b29107 --- /dev/null +++ b/src/app/(payload)/admin/importMap.js @@ -0,0 +1,3 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +export const importMap = {}; diff --git a/src/app/(payload)/api/[...slug]/route.ts b/src/app/(payload)/api/[...slug]/route.ts new file mode 100644 index 0000000..dc8ba37 --- /dev/null +++ b/src/app/(payload)/api/[...slug]/route.ts @@ -0,0 +1,19 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import config from "@payload-config"; +import "@payloadcms/next/css"; +import { + REST_DELETE, + REST_GET, + REST_OPTIONS, + REST_PATCH, + REST_POST, + REST_PUT, +} from "@payloadcms/next/routes"; + +export const GET = REST_GET(config); +export const POST = REST_POST(config); +export const DELETE = REST_DELETE(config); +export const PATCH = REST_PATCH(config); +export const PUT = REST_PUT(config); +export const OPTIONS = REST_OPTIONS(config); diff --git a/src/app/(payload)/custom.scss b/src/app/(payload)/custom.scss new file mode 100644 index 0000000..a8d95bd --- /dev/null +++ b/src/app/(payload)/custom.scss @@ -0,0 +1 @@ +/* Custom styles for the Payload admin panel - can be extended later */ diff --git a/src/app/(payload)/layout.tsx b/src/app/(payload)/layout.tsx new file mode 100644 index 0000000..3c6b6e4 --- /dev/null +++ b/src/app/(payload)/layout.tsx @@ -0,0 +1,31 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import config from "@payload-config"; +import "@payloadcms/next/css"; +import type { ServerFunctionClient } from "payload"; +import { handleServerFunctions, RootLayout } from "@payloadcms/next/layouts"; +import React from "react"; + +import { importMap } from "./admin/importMap.js"; +import "./custom.scss"; + +type Args = { + children: React.ReactNode; +}; + +const serverFunction: ServerFunctionClient = async function (args) { + "use server"; + return handleServerFunctions({ + ...args, + config, + importMap, + }); +}; + +const Layout = ({ children }: Args) => ( + <RootLayout config={config} importMap={importMap} serverFunction={serverFunction}> + {children} + </RootLayout> +); + +export default Layout; diff --git a/src/app/(public)/about/page.tsx b/src/app/(public)/about/page.tsx new file mode 100644 index 0000000..7e76cbe --- /dev/null +++ b/src/app/(public)/about/page.tsx @@ -0,0 +1,153 @@ +import { Code, Server, Shield, Award } from "lucide-react"; + +const timeline = [ + { + icon: Code, + period: "Début de carrière", + title: "Développeur", + description: + "Développement logiciel, compréhension profonde du code et des architectures applicatives. Base solide pour comprendre les enjeux de sécurité au niveau applicatif.", + }, + { + icon: Server, + period: "Évolution", + title: "Administrateur Systèmes", + description: + "Administration Linux et Windows Server. Gestion d'infrastructures, scripting, automatisation. Vision complète de la chaîne technique.", + }, + { + icon: Shield, + period: "Spécialisation", + title: "Expert IAM & Sécurité", + description: + "Spécialisation en Identity & Access Management. Administration AD et Entra ID, implémentation OIDC/OAuth, stratégies Zero Trust.", + }, +]; + +const skills = { + "Identity & Access Management": [ + "OIDC / OAuth 2.0 / SAML", + "Keycloak", + "Active Directory", + "Microsoft Entra ID (Azure AD)", + "PIM / PAM", + "RBAC / ABAC", + "Conditional Access", + "SSO / Federation", + ], + "Sécurité": [ + "Zero Trust Architecture", + "Durcissement AD (Tiering Model)", + "Sécurité des endpoints", + "PKI & Certificats", + "Audit de sécurité", + "Conformité & Gouvernance", + ], + "Systèmes & Infra": [ + "Windows Server / AD DS", + "Linux (Debian, RHEL, Ubuntu)", + "Docker & Conteneurisation", + "PowerShell / Bash", + "Automatisation & CI/CD", + "Monitoring & Logging", + ], +}; + +export default function AboutPage() { + return ( + <> + <section className="bg-cosmos-900 py-16 sm:py-20"> + <div className="mx-auto max-w-7xl px-6 lg:px-8"> + <div className="mx-auto max-w-2xl text-center"> + <h1 className="text-3xl font-bold tracking-tight text-nieve sm:text-5xl"> + À propos + </h1> + <p className="mt-6 text-lg text-cosmos-300"> + Un parcours du développement à la sécurité, pour une vision + complète et pragmatique de l'IAM. + </p> + </div> + </div> + </section> + + {/* Timeline */} + <section className="py-20"> + <div className="mx-auto max-w-7xl px-6 lg:px-8"> + <h2 className="text-2xl font-bold text-cosmos-900 text-center mb-16"> + Parcours professionnel + </h2> + <div className="relative"> + <div className="absolute left-1/2 -translate-x-px h-full w-0.5 bg-border hidden lg:block" /> + <div className="space-y-12 lg:space-y-16"> + {timeline.map((item, index) => { + const Icon = item.icon; + const isLeft = index % 2 === 0; + return ( + <div + key={item.title} + className={`relative flex flex-col lg:flex-row items-center gap-8 ${ + isLeft ? "lg:flex-row" : "lg:flex-row-reverse" + }`} + > + <div + className={`flex-1 ${isLeft ? "lg:text-right" : "lg:text-left"}`} + > + <p className="text-sm font-semibold text-araucaria-600 uppercase tracking-wider"> + {item.period} + </p> + <h3 className="mt-2 text-xl font-bold text-cosmos-900"> + {item.title} + </h3> + <p className="mt-3 text-muted leading-relaxed"> + {item.description} + </p> + </div> + <div className="relative z-10 w-14 h-14 rounded-full bg-cosmos-900 flex items-center justify-center border-4 border-nieve shadow-lg shrink-0"> + <Icon className="w-6 h-6 text-araucaria-400" /> + </div> + <div className="flex-1 hidden lg:block" /> + </div> + ); + })} + </div> + </div> + </div> + </section> + + {/* Compétences */} + <section className="py-20 bg-pewma border-t border-border"> + <div className="mx-auto max-w-7xl px-6 lg:px-8"> + <h2 className="text-2xl font-bold text-cosmos-900 text-center mb-16"> + Compétences techniques + </h2> + <div className="grid grid-cols-1 md:grid-cols-3 gap-8"> + {Object.entries(skills).map(([category, items]) => ( + <div + key={category} + className="rounded-xl border border-border bg-nieve p-8" + > + <div className="flex items-center gap-3 mb-6"> + <Award className="w-5 h-5 text-kultrun-700" /> + <h3 className="text-lg font-semibold text-cosmos-900"> + {category} + </h3> + </div> + <ul className="space-y-2"> + {items.map((skill) => ( + <li + key={skill} + className="text-sm text-muted flex items-center gap-2" + > + <span className="w-1.5 h-1.5 rounded-full bg-araucaria-500 shrink-0" /> + {skill} + </li> + ))} + </ul> + </div> + ))} + </div> + </div> + </section> + </> + ); +} diff --git a/src/app/(public)/articles/page.tsx b/src/app/(public)/articles/page.tsx new file mode 100644 index 0000000..5dd7a01 --- /dev/null +++ b/src/app/(public)/articles/page.tsx @@ -0,0 +1,38 @@ +import { FileText } from "lucide-react"; + +export default function ArticlesPage() { + return ( + <> + <section className="bg-cosmos-900 py-16 sm:py-20"> + <div className="mx-auto max-w-7xl px-6 lg:px-8"> + <div className="mx-auto max-w-2xl text-center"> + <h1 className="text-3xl font-bold tracking-tight text-nieve sm:text-5xl"> + Articles & Guides + </h1> + <p className="mt-6 text-lg text-cosmos-300"> + Concepts de sécurité, guides d'implémentation OIDC/OAuth, + best practices Zero Trust. + </p> + </div> + </div> + </section> + + <section className="py-20"> + <div className="mx-auto max-w-7xl px-6 lg:px-8"> + <div className="mx-auto max-w-2xl text-center"> + <div className="w-16 h-16 mx-auto rounded-2xl bg-pewma flex items-center justify-center mb-6"> + <FileText className="w-8 h-8 text-cosmos-500" /> + </div> + <h2 className="text-xl font-semibold text-cosmos-900"> + Articles à venir + </h2> + <p className="mt-4 text-muted"> + Les premiers articles techniques sur OIDC, OAuth2 et le Zero Trust + sont en cours de rédaction. Revenez bientôt ! + </p> + </div> + </div> + </section> + </> + ); +} diff --git a/src/app/(public)/demos/page.tsx b/src/app/(public)/demos/page.tsx new file mode 100644 index 0000000..7afee2a --- /dev/null +++ b/src/app/(public)/demos/page.tsx @@ -0,0 +1,94 @@ +import Link from "next/link"; +import { Workflow, Terminal, KeyRound, ShieldCheck } from "lucide-react"; + +const demos = [ + { + icon: Workflow, + title: "Visualiseur OIDC", + description: + "Animation pas-à-pas des flux OIDC : Authorization Code, PKCE, Client Credentials. Comprenez chaque échange entre le client, l'IdP et le resource server.", + href: "/demos/oidc-flow", + status: "Bientôt disponible", + }, + { + icon: Terminal, + title: "Playground OAuth2", + description: + "Testez interactivement les requêtes OAuth2. Explorez les scopes, tokens, refresh tokens. Connecté à un vrai serveur Keycloak.", + href: "/demos/oauth-playground", + status: "Bientôt disponible", + }, + { + icon: KeyRound, + title: "Décodeur JWT", + description: + "Décodez et inspectez vos tokens JWT en temps réel. Visualisez le header, le payload et validez la signature.", + href: "/demos/token-decoder", + status: "Bientôt disponible", + }, + { + icon: ShieldCheck, + title: "Simulateur Zero Trust", + description: + "Visualisation interactive des couches de sécurité Zero Trust. Explorez les différentes stratégies d'implémentation.", + href: "/demos/zero-trust", + status: "Bientôt disponible", + }, +]; + +export default function DemosPage() { + return ( + <> + <section className="bg-cosmos-900 py-16 sm:py-20"> + <div className="mx-auto max-w-7xl px-6 lg:px-8"> + <div className="mx-auto max-w-2xl text-center"> + <h1 className="text-3xl font-bold tracking-tight text-nieve sm:text-5xl"> + Démos interactives + </h1> + <p className="mt-6 text-lg text-cosmos-300"> + Explorez les concepts de sécurité et d'IAM à travers des + outils interactifs. Apprenez en pratiquant. + </p> + </div> + </div> + </section> + + <section className="py-20"> + <div className="mx-auto max-w-7xl px-6 lg:px-8"> + <div className="grid grid-cols-1 md:grid-cols-2 gap-8"> + {demos.map((demo) => { + const Icon = demo.icon; + return ( + <div + key={demo.title} + className="group relative rounded-xl border border-border bg-nieve p-8 hover:border-cosmos-300 hover:shadow-lg transition-all duration-300" + > + <div className="flex items-start justify-between mb-6"> + <div className="w-12 h-12 rounded-lg bg-cosmos-900 flex items-center justify-center"> + <Icon className="w-6 h-6 text-araucaria-400" /> + </div> + <span className="px-3 py-1 text-xs font-medium bg-araucaria-50 text-araucaria-700 rounded-full border border-araucaria-200"> + {demo.status} + </span> + </div> + <h3 className="text-xl font-semibold text-cosmos-900"> + {demo.title} + </h3> + <p className="mt-3 text-sm text-muted leading-relaxed"> + {demo.description} + </p> + <Link + href={demo.href} + className="mt-6 inline-flex items-center text-sm font-semibold text-cosmos-700 hover:text-kultrun-700 transition-colors" + > + Explorer la démo → + </Link> + </div> + ); + })} + </div> + </div> + </section> + </> + ); +} diff --git a/src/app/(public)/layout.tsx b/src/app/(public)/layout.tsx new file mode 100644 index 0000000..5b71246 --- /dev/null +++ b/src/app/(public)/layout.tsx @@ -0,0 +1,16 @@ +import { Header } from "@/components/layout/Header"; +import { Footer } from "@/components/layout/Footer"; + +export default function PublicLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> + <Header /> + <main className="min-h-[calc(100vh-4rem)]">{children}</main> + <Footer /> + </> + ); +} diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx new file mode 100644 index 0000000..36ecee7 --- /dev/null +++ b/src/app/(public)/page.tsx @@ -0,0 +1,174 @@ +import Link from "next/link"; +import { + Shield, + Key, + Lock, + Network, + Server, + ArrowRight, + CheckCircle, +} from "lucide-react"; + +const expertises = [ + { + icon: Key, + title: "OIDC & OAuth2", + description: + "Intégration et sécurisation des flux d'authentification. Authorization Code, PKCE, Client Credentials.", + }, + { + icon: Shield, + title: "Zero Trust", + description: + "Architecture Zero Trust de bout en bout. Never trust, always verify. Micro-segmentation et contrôle d'accès continu.", + }, + { + icon: Lock, + title: "Active Directory & Entra ID", + description: + "Administration, durcissement et migration AD. Configuration Entra ID, Conditional Access, PIM.", + }, + { + icon: Network, + title: "IAM & Gouvernance", + description: + "Stratégie IAM complète. Gestion des identités, provisioning, RBAC/ABAC, audit et conformité.", + }, + { + icon: Server, + title: "Infrastructure & DevSecOps", + description: + "Sécurisation Linux & Windows. Automatisation, durcissement, monitoring sécurité.", + }, +]; + +const highlights = [ + "Expert senior avec expérience en développement, systèmes et sécurité", + "Maîtrise des environnements Open Source et Windows", + "Approche pragmatique orientée résultats", + "Accompagnement de la stratégie à l'implémentation", +]; + +export default function HomePage() { + return ( + <> + {/* Hero */} + <section className="relative overflow-hidden bg-cosmos-900 py-24 sm:py-32"> + <div className="absolute inset-0 opacity-10"> + <div + className="absolute inset-0" + style={{ + backgroundImage: `radial-gradient(circle at 25% 25%, var(--color-araucaria-500) 1px, transparent 1px), + radial-gradient(circle at 75% 75%, var(--color-kultrun-700) 1px, transparent 1px)`, + backgroundSize: "60px 60px", + }} + /> + </div> + <div className="relative mx-auto max-w-7xl px-6 lg:px-8"> + <div className="mx-auto max-w-3xl text-center"> + <p className="text-sm font-semibold text-araucaria-400 uppercase tracking-widest"> + Consulting IAM & Sécurité + </p> + <h1 className="mt-4 text-4xl font-bold tracking-tight text-nieve sm:text-6xl"> + Sécurisez vos identités, + <br /> + <span className="text-araucaria-400">protégez vos accès</span> + </h1> + <p className="mt-6 text-lg leading-8 text-cosmos-300"> + Expert senior en Identity & Access Management. J'accompagne + les organisations dans leur stratégie de sécurité des identités, + de l'audit à l'implémentation. + </p> + <div className="mt-10 flex items-center justify-center gap-4 flex-wrap"> + <Link + href="/services" + className="inline-flex items-center gap-2 rounded-lg bg-kultrun-700 px-6 py-3 text-sm font-semibold text-nieve hover:bg-kultrun-600 transition-colors" + > + Mes services + <ArrowRight className="w-4 h-4" /> + </Link> + <Link + href="/demos" + className="inline-flex items-center gap-2 rounded-lg border border-cosmos-600 px-6 py-3 text-sm font-semibold text-cosmos-200 hover:bg-cosmos-800 hover:text-nieve transition-colors" + > + Voir les démos + </Link> + </div> + </div> + </div> + </section> + + {/* Highlights */} + <section className="py-12 bg-pewma border-b border-border"> + <div className="mx-auto max-w-7xl px-6 lg:px-8"> + <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> + {highlights.map((text) => ( + <div key={text} className="flex items-start gap-3"> + <CheckCircle className="w-5 h-5 text-ngunechen-700 mt-0.5 shrink-0" /> + <p className="text-sm text-tierra-700 font-medium">{text}</p> + </div> + ))} + </div> + </div> + </section> + + {/* Expertises */} + <section className="py-20 sm:py-28"> + <div className="mx-auto max-w-7xl px-6 lg:px-8"> + <div className="mx-auto max-w-2xl text-center"> + <h2 className="text-3xl font-bold tracking-tight text-cosmos-900 sm:text-4xl"> + Domaines d'expertise + </h2> + <p className="mt-4 text-lg text-muted"> + Une expertise transversale, du développement à + l'infrastructure, centrée sur la sécurité des identités. + </p> + </div> + + <div className="mx-auto mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3"> + {expertises.map((item) => { + const Icon = item.icon; + return ( + <div + key={item.title} + className="group relative rounded-xl border border-border bg-nieve p-8 hover:border-cosmos-300 hover:shadow-lg transition-all duration-300" + > + <div className="w-12 h-12 rounded-lg bg-cosmos-900 flex items-center justify-center group-hover:bg-cosmos-800 transition-colors"> + <Icon className="w-6 h-6 text-araucaria-400" /> + </div> + <h3 className="mt-6 text-lg font-semibold text-cosmos-900"> + {item.title} + </h3> + <p className="mt-2 text-sm text-muted leading-relaxed"> + {item.description} + </p> + </div> + ); + })} + </div> + </div> + </section> + + {/* CTA */} + <section className="bg-cosmos-900 py-16"> + <div className="mx-auto max-w-7xl px-6 lg:px-8"> + <div className="mx-auto max-w-2xl text-center"> + <h2 className="text-2xl font-bold text-nieve sm:text-3xl"> + Un projet de sécurisation ? + </h2> + <p className="mt-4 text-cosmos-300"> + Discutons de vos besoins en IAM et sécurité informatique. + </p> + <Link + href="/contact" + className="mt-8 inline-flex items-center gap-2 rounded-lg bg-araucaria-500 px-8 py-3 text-sm font-semibold text-cosmos-950 hover:bg-araucaria-400 transition-colors" + > + Prendre contact + <ArrowRight className="w-4 h-4" /> + </Link> + </div> + </div> + </section> + </> + ); +} diff --git a/src/app/(public)/services/page.tsx b/src/app/(public)/services/page.tsx new file mode 100644 index 0000000..424e939 --- /dev/null +++ b/src/app/(public)/services/page.tsx @@ -0,0 +1,148 @@ +import { + Search, + Key, + ShieldCheck, + Server, + ArrowRight, +} from "lucide-react"; +import Link from "next/link"; + +const services = [ + { + id: "audit", + icon: Search, + title: "Audit IAM & Sécurité", + description: + "Évaluation complète de votre posture de sécurité des identités. Analyse des configurations AD, Entra ID, politiques d'accès, et recommandations priorisées.", + deliverables: [ + "Rapport d'audit détaillé", + "Matrice de risques priorisée", + "Plan de remédiation", + ], + }, + { + id: "oidc", + icon: Key, + title: "Intégration OIDC / OAuth2", + description: + "Conception et implémentation de flux d'authentification modernes. Intégration d'applications existantes avec OIDC, migration depuis SAML, mise en place de SSO.", + deliverables: [ + "Architecture d'authentification", + "Implémentation et tests", + "Documentation technique", + ], + }, + { + id: "zero-trust", + icon: ShieldCheck, + title: "Stratégie Zero Trust", + description: + "Définition et déploiement d'une architecture Zero Trust adaptée à votre contexte. Conditional Access, micro-segmentation, vérification continue.", + deliverables: [ + "Feuille de route Zero Trust", + "Configuration Conditional Access", + "Formation des équipes", + ], + }, + { + id: "ad-entra", + icon: Server, + title: "AD & Entra ID", + description: + "Administration avancée, durcissement et migration Active Directory. Configuration Entra ID, synchronisation hybride, PIM/PAM.", + deliverables: [ + "Durcissement AD (tiering model)", + "Configuration Entra ID", + "Migration et synchronisation", + ], + }, +]; + +export default function ServicesPage() { + return ( + <> + <section className="bg-cosmos-900 py-16 sm:py-20"> + <div className="mx-auto max-w-7xl px-6 lg:px-8"> + <div className="mx-auto max-w-2xl text-center"> + <h1 className="text-3xl font-bold tracking-tight text-nieve sm:text-5xl"> + Services de consulting + </h1> + <p className="mt-6 text-lg text-cosmos-300"> + Des mandats ciblés pour sécuriser vos identités et vos accès, + de l'audit stratégique à l'implémentation technique. + </p> + </div> + </div> + </section> + + <section className="py-20"> + <div className="mx-auto max-w-7xl px-6 lg:px-8"> + <div className="space-y-16"> + {services.map((service, index) => { + const Icon = service.icon; + const isEven = index % 2 === 0; + return ( + <div + key={service.id} + id={service.id} + className={`flex flex-col lg:flex-row gap-8 lg:gap-16 items-start ${ + isEven ? "" : "lg:flex-row-reverse" + }`} + > + <div className="flex-1"> + <div className="flex items-center gap-4 mb-4"> + <div className="w-12 h-12 rounded-lg bg-cosmos-900 flex items-center justify-center"> + <Icon className="w-6 h-6 text-araucaria-400" /> + </div> + <h2 className="text-2xl font-bold text-cosmos-900"> + {service.title} + </h2> + </div> + <p className="text-muted leading-relaxed"> + {service.description} + </p> + </div> + <div className="flex-1 w-full lg:max-w-sm"> + <div className="rounded-xl border border-border bg-pewma p-6"> + <h3 className="text-sm font-semibold text-cosmos-700 uppercase tracking-wider mb-4"> + Livrables + </h3> + <ul className="space-y-3"> + {service.deliverables.map((item) => ( + <li + key={item} + className="flex items-start gap-3 text-sm text-tierra-700" + > + <ArrowRight className="w-4 h-4 text-kultrun-700 mt-0.5 shrink-0" /> + {item} + </li> + ))} + </ul> + </div> + </div> + </div> + ); + })} + </div> + </div> + </section> + + <section className="bg-pewma py-16 border-t border-border"> + <div className="mx-auto max-w-7xl px-6 lg:px-8 text-center"> + <h2 className="text-2xl font-bold text-cosmos-900"> + Besoin d'un mandat sur mesure ? + </h2> + <p className="mt-4 text-muted"> + Chaque organisation est unique. Discutons de vos besoins spécifiques. + </p> + <Link + href="/contact" + className="mt-8 inline-flex items-center gap-2 rounded-lg bg-kultrun-700 px-8 py-3 text-sm font-semibold text-nieve hover:bg-kultrun-600 transition-colors" + > + Discuter de mon projet + </Link> + </div> + </section> + </> + ); +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..9fa0a21 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,102 @@ +@import "tailwindcss"; + +@theme { + /* === Palette Mapuche Corporate === */ + + /* Primary - Cosmos (bleu-nuit profond) */ + --color-cosmos-50: #e8ecf3; + --color-cosmos-100: #c5cfe0; + --color-cosmos-200: #9fafc9; + --color-cosmos-300: #7990b2; + --color-cosmos-400: #5c79a1; + --color-cosmos-500: #3f6290; + --color-cosmos-600: #355585; + --color-cosmos-700: #294576; + --color-cosmos-800: #1f3664; + --color-cosmos-900: #1b2a4a; + --color-cosmos-950: #0f1829; + + /* Accent - Kultrun Red */ + --color-kultrun-50: #fce8e8; + --color-kultrun-100: #f8c5c6; + --color-kultrun-200: #f09ea0; + --color-kultrun-300: #e8777a; + --color-kultrun-400: #e1595d; + --color-kultrun-500: #d93c40; + --color-kultrun-600: #c4343a; + --color-kultrun-700: #b8282e; + --color-kultrun-800: #9a2026; + --color-kultrun-900: #7a181d; + --color-kultrun-950: #4a0e11; + + /* Secondary - Araucaria Gold */ + --color-araucaria-50: #fbf5e6; + --color-araucaria-100: #f5e6c0; + --color-araucaria-200: #eed596; + --color-araucaria-300: #e7c46c; + --color-araucaria-400: #e1b84d; + --color-araucaria-500: #d4a843; + --color-araucaria-600: #c99a30; + --color-araucaria-700: #b28527; + --color-araucaria-800: #9a6f1f; + --color-araucaria-900: #7a5616; + --color-araucaria-950: #4a340d; + + /* Earth - Tierra */ + --color-tierra-50: #f3ede8; + --color-tierra-100: #e0d3c8; + --color-tierra-200: #ccb7a5; + --color-tierra-300: #b79b82; + --color-tierra-400: #a78568; + --color-tierra-500: #97704f; + --color-tierra-600: #886347; + --color-tierra-700: #6b4c3b; + --color-tierra-800: #573d30; + --color-tierra-900: #3e2b22; + --color-tierra-950: #241913; + + /* Success - Ngünechen Green */ + --color-ngunechen-50: #e8f2eb; + --color-ngunechen-100: #c5dece; + --color-ngunechen-200: #9ec9ae; + --color-ngunechen-300: #77b38e; + --color-ngunechen-400: #5aa375; + --color-ngunechen-500: #3d935c; + --color-ngunechen-600: #358352; + --color-ngunechen-700: #2d5a3d; + --color-ngunechen-800: #244a32; + --color-ngunechen-900: #1a3624; + --color-ngunechen-950: #0f2015; + + /* Semantic surface colors */ + --color-nieve: #faf7f2; + --color-pewma: #ede8df; + --color-border: #d4cfc6; + --color-muted: #7a746b; + --color-text: #1a1614; + + /* Font families */ + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, monospace; + + /* Border radius */ + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; +} + +@layer base { + body { + background-color: var(--color-nieve); + color: var(--color-text); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + ::selection { + background-color: var(--color-cosmos-900); + color: var(--color-nieve); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..857dbf9 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Der-topogo | Consulting IAM & Sécurité", + description: + "Expert senior en Identity & Access Management, sécurité informatique, OIDC/OAuth, Zero Trust. Active Directory, Microsoft Entra ID, Open Source.", + keywords: [ + "IAM", + "sécurité informatique", + "OIDC", + "OAuth", + "Zero Trust", + "Active Directory", + "Entra ID", + "consulting", + "cybersécurité", + ], +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + <html lang="fr"> + <body>{children}</body> + </html> + ); +} diff --git a/src/collections/Articles.ts b/src/collections/Articles.ts new file mode 100644 index 0000000..abe03ee --- /dev/null +++ b/src/collections/Articles.ts @@ -0,0 +1,99 @@ +import type { CollectionConfig } from "payload"; + +export const Articles: CollectionConfig = { + slug: "articles", + admin: { + useAsTitle: "title", + defaultColumns: ["title", "category", "status", "publishedAt"], + }, + access: { + read: ({ req: { user } }) => { + if (user) return true; + return { status: { equals: "published" } }; + }, + create: ({ req: { user } }) => + user?.role === "admin" || user?.role === "editor", + update: ({ req: { user } }) => + user?.role === "admin" || user?.role === "editor", + delete: ({ req: { user } }) => user?.role === "admin", + }, + fields: [ + { + name: "title", + type: "text", + required: true, + }, + { + name: "slug", + type: "text", + required: true, + unique: true, + admin: { + position: "sidebar", + }, + }, + { + name: "excerpt", + type: "textarea", + required: true, + maxLength: 300, + }, + { + name: "category", + type: "select", + required: true, + options: [ + { label: "OIDC / OAuth", value: "oidc-oauth" }, + { label: "Zero Trust", value: "zero-trust" }, + { label: "Active Directory", value: "active-directory" }, + { label: "Entra ID", value: "entra-id" }, + { label: "Keycloak", value: "keycloak" }, + { label: "Sécurité Générale", value: "security-general" }, + { label: "DevSecOps", value: "devsecops" }, + ], + }, + { + name: "content", + type: "richText", + required: true, + }, + { + name: "coverImage", + type: "upload", + relationTo: "media", + }, + { + name: "status", + type: "select", + required: true, + defaultValue: "draft", + options: [ + { label: "Brouillon", value: "draft" }, + { label: "Publié", value: "published" }, + ], + admin: { + position: "sidebar", + }, + }, + { + name: "publishedAt", + type: "date", + admin: { + position: "sidebar", + date: { + pickerAppearance: "dayAndTime", + }, + }, + }, + { + name: "tags", + type: "array", + fields: [ + { + name: "tag", + type: "text", + }, + ], + }, + ], +}; diff --git a/src/collections/Media.ts b/src/collections/Media.ts new file mode 100644 index 0000000..19cba0c --- /dev/null +++ b/src/collections/Media.ts @@ -0,0 +1,34 @@ +import type { CollectionConfig } from "payload"; + +export const Media: CollectionConfig = { + slug: "media", + admin: { + useAsTitle: "alt", + }, + access: { + read: () => true, + create: ({ req: { user } }) => !!user, + update: ({ req: { user } }) => !!user, + delete: ({ req: { user } }) => user?.role === "admin", + }, + upload: { + mimeTypes: ["image/*", "application/pdf"], + staticDir: "media", + imageSizes: [ + { name: "thumbnail", width: 300, height: 300, position: "centre" }, + { name: "card", width: 768, height: 432, position: "centre" }, + { name: "hero", width: 1920, height: 1080, position: "centre" }, + ], + }, + fields: [ + { + name: "alt", + type: "text", + required: true, + }, + { + name: "caption", + type: "text", + }, + ], +}; diff --git a/src/collections/Services.ts b/src/collections/Services.ts new file mode 100644 index 0000000..923e650 --- /dev/null +++ b/src/collections/Services.ts @@ -0,0 +1,51 @@ +import type { CollectionConfig } from "payload"; + +export const Services: CollectionConfig = { + slug: "services", + admin: { + useAsTitle: "title", + }, + access: { + read: () => true, + create: ({ req: { user } }) => user?.role === "admin", + update: ({ req: { user } }) => user?.role === "admin", + delete: ({ req: { user } }) => user?.role === "admin", + }, + fields: [ + { + name: "title", + type: "text", + required: true, + }, + { + name: "slug", + type: "text", + required: true, + unique: true, + }, + { + name: "description", + type: "textarea", + required: true, + }, + { + name: "icon", + type: "text", + admin: { + description: "Nom de l'icône Lucide (ex: shield, key, lock)", + }, + }, + { + name: "details", + type: "richText", + }, + { + name: "order", + type: "number", + defaultValue: 0, + admin: { + position: "sidebar", + }, + }, + ], +}; diff --git a/src/collections/Testimonials.ts b/src/collections/Testimonials.ts new file mode 100644 index 0000000..3c5d532 --- /dev/null +++ b/src/collections/Testimonials.ts @@ -0,0 +1,42 @@ +import type { CollectionConfig } from "payload"; + +export const Testimonials: CollectionConfig = { + slug: "testimonials", + admin: { + useAsTitle: "clientName", + }, + access: { + read: () => true, + create: ({ req: { user } }) => user?.role === "admin", + update: ({ req: { user } }) => user?.role === "admin", + delete: ({ req: { user } }) => user?.role === "admin", + }, + fields: [ + { + name: "clientName", + type: "text", + required: true, + }, + { + name: "company", + type: "text", + }, + { + name: "role", + type: "text", + }, + { + name: "quote", + type: "textarea", + required: true, + }, + { + name: "featured", + type: "checkbox", + defaultValue: false, + admin: { + position: "sidebar", + }, + }, + ], +}; diff --git a/src/collections/Users.ts b/src/collections/Users.ts new file mode 100644 index 0000000..e7489ac --- /dev/null +++ b/src/collections/Users.ts @@ -0,0 +1,40 @@ +import type { CollectionConfig } from "payload"; + +export const Users: CollectionConfig = { + slug: "users", + admin: { + useAsTitle: "email", + }, + auth: true, + access: { + read: ({ req: { user } }) => !!user, + create: ({ req: { user } }) => user?.role === "admin", + update: ({ req: { user }, id }) => + user?.role === "admin" || user?.id === id, + delete: ({ req: { user } }) => user?.role === "admin", + }, + fields: [ + { + name: "role", + type: "select", + required: true, + defaultValue: "viewer", + options: [ + { label: "Admin", value: "admin" }, + { label: "Editor", value: "editor" }, + { label: "Viewer", value: "viewer" }, + ], + access: { + update: ({ req: { user } }) => user?.role === "admin", + }, + }, + { + name: "firstName", + type: "text", + }, + { + name: "lastName", + type: "text", + }, + ], +}; diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx new file mode 100644 index 0000000..472e029 --- /dev/null +++ b/src/components/layout/Footer.tsx @@ -0,0 +1,113 @@ +import Link from "next/link"; +import { Shield, Mail, Linkedin, Github } from "lucide-react"; + +const footerLinks = { + services: [ + { name: "Audit IAM", href: "/services#audit" }, + { name: "Intégration OIDC", href: "/services#oidc" }, + { name: "Migration Zero Trust", href: "/services#zero-trust" }, + { name: "Sécurisation AD / Entra", href: "/services#ad-entra" }, + ], + resources: [ + { name: "Articles", href: "/articles" }, + { name: "Démo OIDC", href: "/demos/oidc-flow" }, + { name: "Démo OAuth", href: "/demos/oauth-playground" }, + { name: "Démo Zero Trust", href: "/demos/zero-trust" }, + ], +}; + +export function Footer() { + return ( + <footer className="bg-cosmos-950 border-t border-cosmos-800"> + <div className="mx-auto max-w-7xl px-6 py-12 lg:px-8"> + <div className="grid grid-cols-1 md:grid-cols-4 gap-8"> + <div className="md:col-span-1"> + <Link href="/" className="flex items-center gap-3"> + <div className="w-9 h-9 rounded-lg bg-araucaria-500 flex items-center justify-center"> + <Shield className="w-5 h-5 text-cosmos-950" /> + </div> + <span className="text-lg font-bold text-nieve">Der-topogo</span> + </Link> + <p className="mt-4 text-sm text-cosmos-400 leading-relaxed"> + Consulting senior en Identity & Access Management et sécurité + informatique. Expert OIDC, OAuth, Zero Trust, AD, Entra ID. + </p> + </div> + + <div> + <h3 className="text-sm font-semibold text-araucaria-400 uppercase tracking-wider"> + Services + </h3> + <ul className="mt-4 space-y-3"> + {footerLinks.services.map((link) => ( + <li key={link.name}> + <Link + href={link.href} + className="text-sm text-cosmos-300 hover:text-nieve transition-colors" + > + {link.name} + </Link> + </li> + ))} + </ul> + </div> + + <div> + <h3 className="text-sm font-semibold text-araucaria-400 uppercase tracking-wider"> + Ressources + </h3> + <ul className="mt-4 space-y-3"> + {footerLinks.resources.map((link) => ( + <li key={link.name}> + <Link + href={link.href} + className="text-sm text-cosmos-300 hover:text-nieve transition-colors" + > + {link.name} + </Link> + </li> + ))} + </ul> + </div> + + <div> + <h3 className="text-sm font-semibold text-araucaria-400 uppercase tracking-wider"> + Contact + </h3> + <div className="mt-4 space-y-3"> + <a + href="mailto:contact@your-domain.com" + className="flex items-center gap-2 text-sm text-cosmos-300 hover:text-nieve transition-colors" + > + <Mail className="w-4 h-4" /> + contact@your-domain.com + </a> + <div className="flex gap-4 pt-2"> + <a + href="#" + className="text-cosmos-400 hover:text-nieve transition-colors" + aria-label="LinkedIn" + > + <Linkedin className="w-5 h-5" /> + </a> + <a + href="#" + className="text-cosmos-400 hover:text-nieve transition-colors" + aria-label="GitHub" + > + <Github className="w-5 h-5" /> + </a> + </div> + </div> + </div> + </div> + + <div className="mt-12 pt-8 border-t border-cosmos-800"> + <p className="text-xs text-cosmos-500 text-center"> + © {new Date().getFullYear()} Der-topogo. Tous droits réservés. + </p> + </div> + </div> + </footer> + ); +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..01cc271 --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { Menu, X, Shield } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const navigation = [ + { name: "Accueil", href: "/" }, + { name: "Services", href: "/services" }, + { name: "Articles", href: "/articles" }, + { name: "Démos", href: "/demos" }, + { name: "À propos", href: "/about" }, +]; + +export function Header() { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + return ( + <header className="sticky top-0 z-50 bg-cosmos-900/95 backdrop-blur-sm border-b border-cosmos-800"> + <nav className="mx-auto max-w-7xl px-6 lg:px-8" aria-label="Navigation principale"> + <div className="flex h-16 items-center justify-between"> + <Link href="/" className="flex items-center gap-3 group"> + <div className="w-9 h-9 rounded-lg bg-araucaria-500 flex items-center justify-center group-hover:bg-araucaria-400 transition-colors"> + <Shield className="w-5 h-5 text-cosmos-950" /> + </div> + <span className="text-lg font-bold text-nieve tracking-tight"> + Der-topogo + </span> + </Link> + + <div className="hidden md:flex md:items-center md:gap-1"> + {navigation.map((item) => ( + <Link + key={item.name} + href={item.href} + className="px-4 py-2 text-sm font-medium text-cosmos-200 hover:text-nieve hover:bg-cosmos-800 rounded-lg transition-colors" + > + {item.name} + </Link> + ))} + <Link + href="/contact" + className="ml-4 px-5 py-2 text-sm font-semibold bg-kultrun-700 text-nieve rounded-lg hover:bg-kultrun-600 transition-colors" + > + Contact + </Link> + </div> + + <button + type="button" + className="md:hidden p-2 text-cosmos-200 hover:text-nieve" + onClick={() => setMobileMenuOpen(!mobileMenuOpen)} + > + <span className="sr-only">Ouvrir le menu</span> + {mobileMenuOpen ? ( + <X className="h-6 w-6" /> + ) : ( + <Menu className="h-6 w-6" /> + )} + </button> + </div> + + <div + className={cn( + "md:hidden overflow-hidden transition-all duration-300", + mobileMenuOpen ? "max-h-96 pb-4" : "max-h-0" + )} + > + <div className="space-y-1 pt-2"> + {navigation.map((item) => ( + <Link + key={item.name} + href={item.href} + className="block px-4 py-3 text-base font-medium text-cosmos-200 hover:text-nieve hover:bg-cosmos-800 rounded-lg transition-colors" + onClick={() => setMobileMenuOpen(false)} + > + {item.name} + </Link> + ))} + <Link + href="/contact" + className="block px-4 py-3 text-base font-semibold text-kultrun-300 hover:text-kultrun-200 hover:bg-cosmos-800 rounded-lg transition-colors" + onClick={() => setMobileMenuOpen(false)} + > + Contact + </Link> + </div> + </div> + </nav> + </header> + ); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..74f5aed --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,40 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function middleware(request: NextRequest) { + const response = NextResponse.next(); + const { pathname } = request.nextUrl; + + const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); + + const cspDirectives = [ + "default-src 'self'", + `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`, + `style-src 'self' 'unsafe-inline'`, + "img-src 'self' data: blob:", + "font-src 'self'", + `connect-src 'self' ${process.env.KEYCLOAK_ISSUER || ""}`, + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self'", + "upgrade-insecure-requests", + ]; + + if (pathname.startsWith("/admin")) { + return response; + } + + response.headers.set( + "Content-Security-Policy", + cspDirectives.join("; ") + ); + response.headers.set("X-Nonce", nonce); + + return response; +} + +export const config = { + matcher: [ + "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", + ], +}; diff --git a/src/payload.config.ts b/src/payload.config.ts new file mode 100644 index 0000000..422450d --- /dev/null +++ b/src/payload.config.ts @@ -0,0 +1,34 @@ +import { buildConfig } from "payload"; +import { postgresAdapter } from "@payloadcms/db-postgres"; +import { lexicalEditor } from "@payloadcms/richtext-lexical"; +import path from "path"; +import { fileURLToPath } from "url"; + +import { Users } from "./collections/Users"; +import { Articles } from "./collections/Articles"; +import { Services } from "./collections/Services"; +import { Testimonials } from "./collections/Testimonials"; +import { Media } from "./collections/Media"; + +const filename = fileURLToPath(import.meta.url); +const dirname = path.dirname(filename); + +export default buildConfig({ + admin: { + user: Users.slug, + meta: { + titleSuffix: " | Der-topogo Admin", + }, + }, + collections: [Users, Articles, Services, Testimonials, Media], + editor: lexicalEditor(), + secret: process.env.PAYLOAD_SECRET || "CHANGE-ME-IN-PRODUCTION", + typescript: { + outputFile: path.resolve(dirname, "payload-types.ts"), + }, + db: postgresAdapter({ + pool: { + connectionString: process.env.DATABASE_URI || "", + }, + }), +}); |
