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 4 intègre un serveur HTTP (Nitro) qui tourne côté serveur. Les fichiers dans server/api/ deviennent automatiquement des endpoints REST — pas besoin d'Express, pas de fichier de routing.
Le nom du fichier définit la route et la méthode HTTP :
server/api/auth.post.ts → POST /api/auth
server/api/auth/verify.get.ts → GET /api/auth/verify
server/api/proxies.get.ts → GET /api/proxies
server/api/proxies.post.ts → POST /api/proxies
server/api/health.get.ts → GET /api/health
server/api/caddy/health.get.ts → GET /api/caddy/healthUn même fichier peut gérer plusieurs méthodes si on omet le suffixe .method et qu'on exporte un handler default avec un switch sur getMethod(event).
export default defineEventHandler(async (event) => {
return {
status: 'ok',
timestamp: new Date().toISOString(),
version: '1.0.0',
}
})defineEventHandler est le wrapper Nitro/h3. Il reçoit un event qui contient la requête, la réponse, et les utilitaires.
import { readBody } from 'h3'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// body est déjà parsé depuis le JSON
const { name, domain, target } = body
// ...
})readBody parse automatiquement le JSON. Pas de JSON.parse manuel.
import { getQuery } from 'h3'
export default defineEventHandler(async (event) => {
const { search, status } = getQuery(event)
// search et status viennent de ?search=...&status=...
})import { getHeader } from 'h3'
export default defineEventHandler(async (event) => {
const authHeader = getHeader(event, 'authorization')
// "Bearer eyJhbGci..."
})export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
// config.jwtSecret, config.adminUsername, config.adminPassword
// config.caddyAdminUrl
})useRuntimeConfig() côté serveur expose les secrets. Côté client, seul config.public.* est accessible.
import { createError } from 'h3'
throw createError({
statusCode: 401,
statusMessage: 'Non autorisé',
})Nitro sérialise l'erreur en JSON et renvoie le bon status HTTP. Pas besoin de res.status(401).json(...).
Les fichiers dans server/utils/ sont auto-importés dans tous les handlers. C'est là que vit le client Caddy et la vérification JWT :
import jwt from 'jsonwebtoken'
import { getHeader, createError } from 'h3'
export function authenticateToken(event: H3Event) {
const config = useRuntimeConfig()
const authHeader = getHeader(event, 'authorization')
if (!authHeader?.startsWith('Bearer ')) {
throw createError({ statusCode: 401, statusMessage: 'Token manquant' })
}
const token = authHeader.slice(7)
try {
return jwt.verify(token, config.jwtSecret) as JwtPayload
} catch {
throw createError({ statusCode: 403, statusMessage: 'Token invalide' })
}
}authenticateToken(event) s'appelle en première ligne de chaque handler protégé :
export default defineEventHandler(async (event) => {
const user = authenticateToken(event) // 401/403 si invalide
// ici, l'utilisateur est authentifié
const proxies = await getCaddyProxies()
return { success: true, data: proxies }
})Pour des traitements transversaux (logging, CORS), créer server/middleware/ :
export default defineEventHandler((event) => {
setResponseHeaders(event, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
})
})Ce middleware s'exécute sur toutes les requêtes, avant les handlers. Pas de config de routing.