Construire un dashboard Nuxt 4 pour piloter Caddy via son Admin API — authentification JWT, reverse proxy CRUD, import/export, et déploiement Docker Compose.
Quatre pages, un layout principal avec sidebar, et quelques composants réutilisables. Tout est TypeScript strict, tout est Composition API.
<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><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><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><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.