iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
TypeScript · Avancé

OpenCaddyHost

Construire un dashboard Nuxt 4 pour piloter Caddy via son Admin API — authentification JWT, reverse proxy CRUD, import/export, et déploiement Docker Compose.

Nuxt 4Vue 3TypeScriptPiniaCaddyDocker
GitHub
01Setup et configuration02L'API Admin de Caddy03Server routes Nuxt 404Authentification JWT05Client Caddy et gestion des proxies06Interface et composants Vue07Déploiement Docker Compose
Chapitre 6·25 min

Interface et composants Vue

Quatre pages, un layout principal avec sidebar, et quelques composants réutilisables. Tout est TypeScript strict, tout est Composition API.

Layout principal

app/layouts/default.vue
<script setup lang="ts">
const { logout, user } = useAuth()
const route = useRoute()
 
const navItems = [
  { to: '/', label: 'Dashboard', icon: 'heroicons:home' },
  { to: '/proxies', label: 'Proxies', icon: 'heroicons:server' },
  { to: '/import-export', label: 'Import / Export', icon: 'heroicons:arrow-path' },
  { to: '/settings', label: 'Paramètres', icon: 'heroicons:cog-6-tooth' },
]
</script>
 
<template>
  <div class="flex h-screen bg-gray-900 text-white">
 
    <!-- Sidebar -->
    <aside class="w-64 bg-gray-800 border-r border-gray-700 flex flex-col">
      <div class="p-6 border-b border-gray-700">
        <h1 class="text-xl font-bold text-white">OpenCaddyHost</h1>
        <p class="text-xs text-gray-400 mt-1">{{ user?.username }}</p>
      </div>
 
      <nav class="flex-1 p-4 space-y-1">
        <NuxtLink
          v-for="item in navItems"
          :key="item.to"
          :to="item.to"
          class="flex items-center gap-3 px-4 py-2.5 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700 transition-colors"
          active-class="bg-gray-700 text-white"
        >
          <Icon :name="item.icon" class="w-5 h-5" />
          {{ item.label }}
        </NuxtLink>
      </nav>
 
      <div class="p-4 border-t border-gray-700">
        <button @click="logout" class="flex items-center gap-2 text-gray-400 hover:text-white transition-colors text-sm w-full">
          <Icon name="heroicons:arrow-right-on-rectangle" class="w-4 h-4" />
          Déconnexion
        </button>
      </div>
    </aside>
 
    <!-- Contenu -->
    <main class="flex-1 overflow-auto">
      <slot />
    </main>
 
  </div>
</template>

Page dashboard — index.vue

app/pages/index.vue
<script setup lang="ts">
const { getAuthHeaders } = useAuth()
 
const { data: health } = await useFetch('/api/caddy/health', {
  headers: getAuthHeaders(),
})
const { data: proxiesData } = await useFetch('/api/proxies', {
  headers: getAuthHeaders(),
})
 
const stats = computed(() => ({
  total: proxiesData.value?.data?.length ?? 0,
  healthy: health.value?.healthy ?? false,
}))
</script>
 
<template>
  <div class="p-8">
    <h2 class="text-2xl font-bold mb-6">Dashboard</h2>
 
    <!-- Statut Caddy -->
    <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
      <div class="bg-gray-800 rounded-xl p-6">
        <div class="flex items-center gap-3 mb-2">
          <div :class="['w-2.5 h-2.5 rounded-full', stats.healthy ? 'bg-green-400' : 'bg-red-400']" />
          <span class="text-gray-400 text-sm">Caddy</span>
        </div>
        <p class="text-2xl font-bold">{{ stats.healthy ? 'En ligne' : 'Hors ligne' }}</p>
      </div>
 
      <div class="bg-gray-800 rounded-xl p-6">
        <p class="text-gray-400 text-sm mb-2">Proxies actifs</p>
        <p class="text-2xl font-bold">{{ stats.total }}</p>
      </div>
    </div>
 
    <!-- Liste rapide des proxies -->
    <div class="bg-gray-800 rounded-xl">
      <div class="p-6 border-b border-gray-700">
        <h3 class="font-semibold">Proxies récents</h3>
      </div>
      <div class="divide-y divide-gray-700">
        <div
          v-for="proxy in proxiesData?.data?.slice(0, 5)"
          :key="proxy.id"
          class="p-4 flex items-center justify-between"
        >
          <div>
            <p class="font-medium">{{ proxy.domain }}</p>
            <p class="text-sm text-gray-400">→ {{ proxy.target }}</p>
          </div>
          <NuxtLink to="/proxies" class="text-xs text-blue-400 hover:underline">Gérer</NuxtLink>
        </div>
      </div>
    </div>
  </div>
</template>

Composant ProxyCard

app/components/ProxyCard.vue
<script setup lang="ts">
interface Proxy {
  id: string
  name: string
  domain: string
  target: string
  ssl: boolean
  createdAt?: string
}
 
const props = defineProps<{ proxy: Proxy }>()
const emit = defineEmits<{ delete: [id: string] }>()
</script>
 
<template>
  <div class="bg-gray-800 rounded-xl p-5 border border-gray-700 hover:border-gray-600 transition-colors">
    <div class="flex items-start justify-between">
      <div class="flex-1 min-w-0">
        <p class="font-semibold truncate">{{ proxy.domain }}</p>
        <p class="text-sm text-gray-400 mt-1 truncate">→ {{ proxy.target }}</p>
      </div>
      <span
        :class="['text-xs px-2 py-0.5 rounded ml-3 shrink-0', proxy.ssl ? 'bg-green-900 text-green-300' : 'bg-gray-700 text-gray-400']"
      >
        {{ proxy.ssl ? 'HTTPS' : 'HTTP' }}
      </span>
    </div>
 
    <div class="flex items-center justify-between mt-4 pt-4 border-t border-gray-700">
      <span class="text-xs text-gray-500">
        {{ proxy.createdAt ? new Date(proxy.createdAt).toLocaleDateString('fr-FR') : '—' }}
      </span>
      <button
        @click="emit('delete', proxy.id)"
        class="text-xs text-red-400 hover:text-red-300 transition-colors"
      >
        Supprimer
      </button>
    </div>
  </div>
</template>

Page import-export.vue

app/pages/import-export.vue
<script setup lang="ts">
const { getAuthHeaders } = useAuth()
 
// Export — télécharge la config Caddy en JSON
async function exportConfig() {
  const data = await $fetch('/api/caddy/config', { headers: getAuthHeaders() })
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = `caddy-config-${Date.now()}.json`
  a.click()
  URL.revokeObjectURL(url)
}
 
// Import — charge un fichier JSON et l'envoie à Caddy
const fileInput = ref<HTMLInputElement | null>(null)
 
async function importConfig(event: Event) {
  const file = (event.target as HTMLInputElement).files?.[0]
  if (!file) return
 
  const text = await file.text()
  const config = JSON.parse(text)
 
  await $fetch('/api/caddy/config', {
    method: 'POST',
    headers: getAuthHeaders(),
    body: config,
  })
}
</script>
 
<template>
  <div class="p-8 space-y-6">
    <h2 class="text-2xl font-bold">Import / Export</h2>
 
    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
      <div class="bg-gray-800 rounded-xl p-6">
        <h3 class="font-semibold mb-2">Exporter</h3>
        <p class="text-gray-400 text-sm mb-4">Télécharger la configuration Caddy complète en JSON.</p>
        <button @click="exportConfig" class="btn-primary">Télécharger</button>
      </div>
 
      <div class="bg-gray-800 rounded-xl p-6">
        <h3 class="font-semibold mb-2">Importer</h3>
        <p class="text-gray-400 text-sm mb-4">Charger une configuration JSON — écrase la configuration actuelle.</p>
        <input ref="fileInput" type="file" accept=".json" class="hidden" @change="importConfig" />
        <button @click="fileInput?.click()" class="btn-secondary">Choisir un fichier</button>
      </div>
    </div>
  </div>
</template>

L'import est destructif : il écrase toute la configuration Caddy via /load. Ajouter une confirmation modale avant de l'envoyer en production.

Précédent
Client Caddy et gestion des proxies
Suivant
Déploiement Docker Compose

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main