summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/(payload)/admin/[[...segments]]/not-found.tsx23
-rw-r--r--src/app/(payload)/admin/[[...segments]]/page.tsx23
-rw-r--r--src/app/(payload)/admin/importMap.js3
-rw-r--r--src/app/(payload)/api/[...slug]/route.ts19
-rw-r--r--src/app/(payload)/custom.scss1
-rw-r--r--src/app/(payload)/layout.tsx31
-rw-r--r--src/app/(public)/about/page.tsx153
-rw-r--r--src/app/(public)/articles/page.tsx38
-rw-r--r--src/app/(public)/demos/page.tsx94
-rw-r--r--src/app/(public)/layout.tsx16
-rw-r--r--src/app/(public)/page.tsx174
-rw-r--r--src/app/(public)/services/page.tsx148
-rw-r--r--src/app/globals.css102
-rw-r--r--src/app/layout.tsx31
-rw-r--r--src/collections/Articles.ts99
-rw-r--r--src/collections/Media.ts34
-rw-r--r--src/collections/Services.ts51
-rw-r--r--src/collections/Testimonials.ts42
-rw-r--r--src/collections/Users.ts40
-rw-r--r--src/components/layout/Footer.tsx113
-rw-r--r--src/components/layout/Header.tsx93
-rw-r--r--src/lib/utils.ts6
-rw-r--r--src/middleware.ts40
-rw-r--r--src/payload.config.ts34
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&apos;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&apos;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&apos;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 &rarr;
+ </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&apos;accompagne
+ les organisations dans leur stratégie de sécurité des identités,
+ de l&apos;audit à l&apos;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&apos;expertise
+ </h2>
+ <p className="mt-4 text-lg text-muted">
+ Une expertise transversale, du développement à
+ l&apos;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&apos;audit stratégique à l&apos;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&apos;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">
+ &copy; {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 || "",
+ },
+ }),
+});