From 92d854a31cf1c39f47673ecc26163309c69bf7cd Mon Sep 17 00:00:00 2001 From: defiQUG Date: Fri, 27 Mar 2026 18:50:54 -0700 Subject: [PATCH] phoenix-deploy-api: OpenAPI, server, systemd install, env example Made-with: Cursor --- phoenix-deploy-api/.env.example | 5 ++ phoenix-deploy-api/README.md | 7 ++- phoenix-deploy-api/openapi.yaml | 22 +++++++++ phoenix-deploy-api/scripts/install-systemd.sh | 7 +++ phoenix-deploy-api/server.js | 49 ++++++++++++++++++- 5 files changed, 87 insertions(+), 3 deletions(-) diff --git a/phoenix-deploy-api/.env.example b/phoenix-deploy-api/.env.example index f27b87c..41cbfa0 100644 --- a/phoenix-deploy-api/.env.example +++ b/phoenix-deploy-api/.env.example @@ -28,3 +28,8 @@ PHOENIX_ALERT_WEBHOOK_SECRET= # Optional: comma-separated API keys for /api/v1/* (X-API-Key or Bearer) PHOENIX_PARTNER_KEYS= + +# Optional: override path for GET /api/v1/public-sector/programs (else bundled copy, repo config/, or ../config/) +PUBLIC_SECTOR_MANIFEST_PATH= +# Optional: proxmox repo root on host (manifest = $PHOENIX_REPO_ROOT/config/public-sector-program-manifest.json) +PHOENIX_REPO_ROOT= diff --git a/phoenix-deploy-api/README.md b/phoenix-deploy-api/README.md index a187c54..68eb542 100644 --- a/phoenix-deploy-api/README.md +++ b/phoenix-deploy-api/README.md @@ -16,9 +16,10 @@ Gitea webhook receiver and deploy endpoint stub for Gitea → Phoenix deployment | GET | /api/v1/health/metrics | Prometheus query proxy (`?query=`) | | GET | /api/v1/health/alerts | Active alerts (optional PROMETHEUS_ALERTS_URL) | | GET | /api/v1/health/summary | Aggregated health for Portal | +| GET | /api/v1/public-sector/programs | Public-sector / eIDAS program manifest (JSON; **no API key**) | | GET | /health | Health check | -All `/api/v1/*` routes accept optional partner API key when `PHOENIX_PARTNER_KEYS` is set (`X-API-Key` or `Authorization: Bearer `). +All `/api/v1/*` routes except **`GET /api/v1/public-sector/programs`** accept optional partner API key when `PHOENIX_PARTNER_KEYS` is set (`X-API-Key` or `Authorization: Bearer `). ## Environment @@ -42,6 +43,10 @@ Copy `.env.example` to `.env` and set `GITEA_TOKEN` (and optionally `PHOENIX_DEP | PHOENIX_WEBHOOK_URL | | Outbound webhook URL; POST deploy events with X-Phoenix-Signature | | PHOENIX_WEBHOOK_SECRET | | Secret to sign webhook payloads (HMAC-SHA256) | | PHOENIX_PARTNER_KEYS | | Comma-separated API keys for /api/v1/* (optional) | +| PUBLIC_SECTOR_MANIFEST_PATH | | Override JSON path for `/api/v1/public-sector/programs` | +| PHOENIX_REPO_ROOT | | Proxmox repo root; loads `config/public-sector-program-manifest.json` if present | + +**Program manifest:** From a full repo checkout, the file is `config/public-sector-program-manifest.json`. `scripts/install-systemd.sh` copies it next to `server.js` on `/opt/phoenix-deploy-api` so the endpoint works without a full tree. ## Gitea Webhook Configuration diff --git a/phoenix-deploy-api/openapi.yaml b/phoenix-deploy-api/openapi.yaml index be0e5b3..071c659 100644 --- a/phoenix-deploy-api/openapi.yaml +++ b/phoenix-deploy-api/openapi.yaml @@ -15,6 +15,7 @@ tags: - name: Infra - name: VE - name: Health + - name: PublicSector - name: System paths: @@ -66,6 +67,27 @@ paths: '202': { description: Accepted } '401': { description: Unauthorized } + /api/v1/public-sector/programs: + get: + tags: [PublicSector] + summary: Public-sector and eIDAS program manifest (JSON) + description: | + Serves `config/public-sector-program-manifest.json` from the proxmox repo (or `PUBLIC_SECTOR_MANIFEST_PATH`). + No API key required. Returns 503 if the file is missing on the host. + responses: + '200': + description: Manifest JSON + content: + application/json: + schema: + type: object + properties: + schemaVersion: { type: string } + updated: { type: string } + programs: { type: array, items: { type: object } } + '500': { description: Invalid JSON or read error } + '503': { description: Manifest file not found } + /api/v1/infra/nodes: get: tags: [Infra] diff --git a/phoenix-deploy-api/scripts/install-systemd.sh b/phoenix-deploy-api/scripts/install-systemd.sh index 0f6df6d..6d06cf9 100644 --- a/phoenix-deploy-api/scripts/install-systemd.sh +++ b/phoenix-deploy-api/scripts/install-systemd.sh @@ -18,6 +18,13 @@ fi echo "Installing Phoenix Deploy API to $TARGET ..." mkdir -p "$TARGET" cp -a "$APP_DIR/server.js" "$APP_DIR/package.json" "$APP_DIR/package-lock.json" "$TARGET/" 2>/dev/null || cp -a "$APP_DIR/server.js" "$APP_DIR/package.json" "$TARGET/" +# Program manifest for GET /api/v1/public-sector/programs (server loads from cwd-adjacent copy on /opt) +if [[ -f "$REPO_ROOT/config/public-sector-program-manifest.json" ]]; then + cp -a "$REPO_ROOT/config/public-sector-program-manifest.json" "$TARGET/public-sector-program-manifest.json" + echo "Installed public-sector-program-manifest.json" +else + echo "WARN: $REPO_ROOT/config/public-sector-program-manifest.json missing — set PUBLIC_SECTOR_MANIFEST_PATH in .env" +fi [ -f "$APP_DIR/.env" ] && cp "$APP_DIR/.env" "$TARGET/.env" || [ -f "$APP_DIR/.env.example" ] && cp "$APP_DIR/.env.example" "$TARGET/.env" || true chown -R root:root "$TARGET" cd "$TARGET" && npm install --omit=dev diff --git a/phoenix-deploy-api/server.js b/phoenix-deploy-api/server.js index 57b0e94..dd45c50 100644 --- a/phoenix-deploy-api/server.js +++ b/phoenix-deploy-api/server.js @@ -9,6 +9,7 @@ * GET /api/v1/infra/storage — Storage pools (Proxmox or stub) * GET /api/v1/ve/vms — List VMs/CTs (Proxmox or stub) * GET /api/v1/ve/vms/:node/:vmid/status — VM/CT status + * GET /api/v1/public-sector/programs — Public-sector / eIDAS program manifest (JSON) * GET /health — Health check * * Env: PORT, GITEA_URL, GITEA_TOKEN, PHOENIX_DEPLOY_SECRET @@ -18,7 +19,7 @@ import crypto from 'crypto'; import https from 'https'; import path from 'path'; -import { readFileSync } from 'fs'; +import { readFileSync, existsSync } from 'fs'; import { fileURLToPath } from 'url'; import express from 'express'; @@ -42,6 +43,26 @@ const PHOENIX_WEBHOOK_URL = process.env.PHOENIX_WEBHOOK_URL || ''; const PHOENIX_WEBHOOK_SECRET = process.env.PHOENIX_WEBHOOK_SECRET || ''; const PARTNER_KEYS = (process.env.PHOENIX_PARTNER_KEYS || '').split(',').map((k) => k.trim()).filter(Boolean); +/** + * Manifest resolution order: + * 1) PUBLIC_SECTOR_MANIFEST_PATH (explicit file) + * 2) public-sector-program-manifest.json next to server.js (systemd install copies this) + * 3) PHOENIX_REPO_ROOT or PROXMOX_REPO_PATH + config/public-sector-program-manifest.json + * 4) ../config/... (running from phoenix-deploy-api inside full proxmox clone) + */ +function resolvePublicSectorManifestPath() { + const override = (process.env.PUBLIC_SECTOR_MANIFEST_PATH || '').trim(); + if (override && existsSync(override)) return override; + const bundled = path.join(__dirname, 'public-sector-program-manifest.json'); + if (existsSync(bundled)) return bundled; + const repoRoot = (process.env.PHOENIX_REPO_ROOT || process.env.PROXMOX_REPO_PATH || '').trim().replace(/\/$/, ''); + if (repoRoot) { + const fromRepo = path.join(repoRoot, 'config', 'public-sector-program-manifest.json'); + if (existsSync(fromRepo)) return fromRepo; + } + return path.join(__dirname, '..', 'config', 'public-sector-program-manifest.json'); +} + const httpsAgent = new https.Agent({ rejectUnauthorized: process.env.PROXMOX_TLS_VERIFY !== '0' }); async function proxmoxRequest(endpoint, method = 'GET', body = null) { @@ -207,6 +228,28 @@ app.post('/api/deploy', async (req, res) => { } }); +/** + * GET /api/v1/public-sector/programs — Program registry for operators / Phoenix UI (non-secret). + * Registered before partnerKeyMiddleware so callers do not need X-API-Key. + */ +app.get('/api/v1/public-sector/programs', (req, res) => { + const manifestPath = resolvePublicSectorManifestPath(); + try { + if (!existsSync(manifestPath)) { + return res.status(503).json({ + error: 'Manifest not found', + path: manifestPath, + hint: 'Set PUBLIC_SECTOR_MANIFEST_PATH or deploy repo with config/public-sector-program-manifest.json next to phoenix-deploy-api/', + }); + } + const raw = readFileSync(manifestPath, 'utf8'); + const data = JSON.parse(raw); + res.type('application/json').json(data); + } catch (err) { + res.status(500).json({ error: err.message, path: manifestPath }); + } +}); + app.use('/api/v1', partnerKeyMiddleware); /** @@ -431,5 +474,7 @@ app.listen(PORT, () => { if (!GITEA_TOKEN) console.warn('GITEA_TOKEN not set — commit status updates disabled'); if (!hasProxmox) console.warn('PROXMOX_* not set — Infra/VE API returns stub data'); if (PHOENIX_WEBHOOK_URL) console.log('Outbound webhook enabled:', PHOENIX_WEBHOOK_URL); - if (PARTNER_KEYS.length > 0) console.log('Partner API key auth enabled for /api/v1/*'); + if (PARTNER_KEYS.length > 0) console.log('Partner API key auth enabled for /api/v1/* (except GET /api/v1/public-sector/programs)'); + const mpath = resolvePublicSectorManifestPath(); + console.log(`Public-sector manifest: ${mpath} (${existsSync(mpath) ? 'ok' : 'missing'})`); });