feat: UI vue Review + backend sert le SPA, fin Phase 4 v1 (v0.24.0)
Vue Review (liste assets, approuver/refuser + confirm, refresh, 401→logout). Backend sert le SPA same-origin (@fastify/static + fallback) si CHLOVA_WEB_ROOT. Dockerfile multi-stage build web+API (contexte racine), image embarque /app/web. Compose contexte .., image chlova/backend:0.2.0. 65 tests, 0 vuln, compose OK. Palier de risque : privilégié (surface exposée complète) — non déployée ; auth + CHLOVA_PHASE requis pour activer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+22
-8
@@ -1,21 +1,35 @@
|
||||
# CHLOVA backend — image multi-stage, base épinglée (jamais :latest).
|
||||
# Contexte de build = RACINE du dépôt (voir infra/docker-compose.yml) : l'image
|
||||
# embarque l'API ET le SPA web (servi same-origin).
|
||||
# TODO épingler le digest (node:24.13-bookworm-slim@sha256:...) avant déploiement réel.
|
||||
|
||||
FROM node:24.13-bookworm-slim AS build
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
# ── Build du SPA (web/) ─────────────────────────────────────────────────
|
||||
FROM node:24.13-bookworm-slim AS web-build
|
||||
WORKDIR /web
|
||||
COPY web/package.json web/package-lock.json* ./
|
||||
RUN npm ci
|
||||
COPY tsconfig.json tsconfig.build.json ./
|
||||
COPY src ./src
|
||||
COPY web/ ./
|
||||
RUN npm run build
|
||||
|
||||
# ── Build de l'API (orchestrator/) ──────────────────────────────────────
|
||||
FROM node:24.13-bookworm-slim AS api-build
|
||||
WORKDIR /app
|
||||
COPY orchestrator/package.json orchestrator/package-lock.json* ./
|
||||
RUN npm ci
|
||||
COPY orchestrator/tsconfig.json orchestrator/tsconfig.build.json ./
|
||||
COPY orchestrator/src ./src
|
||||
RUN npm run build
|
||||
|
||||
# ── Runtime ─────────────────────────────────────────────────────────────
|
||||
FROM node:24.13-bookworm-slim AS runtime
|
||||
ENV NODE_ENV=production
|
||||
ENV CHLOVA_WEB_ROOT=/app/web
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
COPY orchestrator/package.json orchestrator/package-lock.json* ./
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
COPY --from=build /app/dist ./dist
|
||||
# Données runtime (SQLite, P2+). L'utilisateur node ne tourne pas en root.
|
||||
COPY --from=api-build /app/dist ./dist
|
||||
COPY --from=web-build /web/dist ./web
|
||||
# Données runtime (SQLite). L'utilisateur node ne tourne pas en root.
|
||||
RUN mkdir -p /app/data && chown -R node:node /app
|
||||
USER node
|
||||
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
|
||||
|
||||
Generated
+165
-2
@@ -8,8 +8,9 @@
|
||||
"name": "chlova-orchestrator",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^11.2.0",
|
||||
"@fastify/rate-limit": "^11.0.0",
|
||||
"@fastify/cors": "11.2.0",
|
||||
"@fastify/rate-limit": "11.0.0",
|
||||
"@fastify/static": "^9.1.3",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"fastify": "5.8.5",
|
||||
"jose": "6.2.3",
|
||||
@@ -503,6 +504,22 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/accept-negotiator": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz",
|
||||
"integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fastify/ajv-compiler": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz",
|
||||
@@ -655,6 +672,53 @@
|
||||
"toad-cache": "^3.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/send": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz",
|
||||
"integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lukeed/ms": "^2.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"fast-decode-uri-component": "^1.0.1",
|
||||
"http-errors": "^2.0.0",
|
||||
"mime": "^3"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/static": {
|
||||
"version": "9.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-9.1.3.tgz",
|
||||
"integrity": "sha512-aXrYtsiryLhRxRNaxNqsn7FUISeb7rB9q4eHUPIot5aeQBLNahnz1m6thzm7JWC1poSGXS9XrX8DvuMivp2hkQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/accept-negotiator": "^2.0.0",
|
||||
"@fastify/send": "^4.0.0",
|
||||
"content-disposition": "^1.0.1",
|
||||
"fastify-plugin": "^5.0.0",
|
||||
"fastq": "^1.17.1",
|
||||
"glob": "^13.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.14",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
|
||||
@@ -1392,6 +1456,15 @@
|
||||
"fastq": "^1.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
||||
@@ -1429,6 +1502,18 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -2076,6 +2161,23 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "13.0.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
|
||||
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^10.2.2",
|
||||
"minipass": "^7.1.3",
|
||||
"path-scurry": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -2556,6 +2658,15 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.5.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
|
||||
"integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -2596,6 +2707,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
||||
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
@@ -2621,6 +2744,30 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -2752,6 +2899,22 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"dependencies": {
|
||||
"@fastify/cors": "11.2.0",
|
||||
"@fastify/rate-limit": "11.0.0",
|
||||
"@fastify/static": "9.1.3",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"fastify": "5.8.5",
|
||||
"jose": "6.2.3",
|
||||
|
||||
@@ -78,6 +78,8 @@ const schema = z.object({
|
||||
(v) => (typeof v === "string" && v.length > 0 ? v : undefined),
|
||||
z.string().url().optional(),
|
||||
), // origine CORS autorisée (dev) ; en prod le SPA est servi en same-origin
|
||||
// Racine du SPA buildé, servi same-origin (vide = ne sert pas de SPA).
|
||||
webRoot: z.string().optional(),
|
||||
});
|
||||
|
||||
export type Config = z.infer<typeof schema>;
|
||||
@@ -116,6 +118,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
|
||||
totpSecret: env.CHLOVA_TOTP_SECRET,
|
||||
jwtSecret: env.CHLOVA_JWT_SECRET,
|
||||
webOrigin: env.CHLOVA_WEB_ORIGIN,
|
||||
webRoot: env.CHLOVA_WEB_ROOT,
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import Fastify, { type FastifyBaseLogger } from "fastify";
|
||||
import fastifyStatic from "@fastify/static";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { loadConfig, assertReadOnlyPhase, redactedConfig, apiAuth } from "./config.js";
|
||||
import { registerApi } from "./api/routes.js";
|
||||
import { createLogger } from "./audit/log.js";
|
||||
@@ -128,6 +131,24 @@ async function main(): Promise<void> {
|
||||
logger.info("API/UI désactivée (auth non configurée) — surface Telegram seule");
|
||||
}
|
||||
|
||||
// SPA same-origin : sert le build web + fallback (routes client) si présent.
|
||||
if (auth && cfg.webRoot) {
|
||||
const root = resolve(cfg.webRoot);
|
||||
if (existsSync(root)) {
|
||||
await app.register(fastifyStatic, { root });
|
||||
app.setNotFoundHandler((req, reply) => {
|
||||
// /api et /health restent gérés ; le reste retombe sur l'app SPA.
|
||||
if (req.url.startsWith("/api") || req.url.startsWith("/health")) {
|
||||
return reply.code(404).send({ error: "not found" });
|
||||
}
|
||||
return reply.sendFile("index.html");
|
||||
});
|
||||
logger.info({ root }, "SPA servi same-origin");
|
||||
} else {
|
||||
logger.warn({ root }, "CHLOVA_WEB_ROOT introuvable — SPA non servi");
|
||||
}
|
||||
}
|
||||
|
||||
await app.listen({ host: "0.0.0.0", port: 8080 });
|
||||
logger.info({ port: 8080 }, "healthcheck interne prêt");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user