From ab504841c3071a57b2175329d3a42536c6cff45b Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Wed, 20 May 2026 15:48:14 -0400 Subject: [PATCH] feat(settings): add S3 / object-storage settings routes (GET, PUT, test) --- services/mam-api/src/routes/settings.js | 118 ++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 8 deletions(-) diff --git a/services/mam-api/src/routes/settings.js b/services/mam-api/src/routes/settings.js index d370941..4d2a390 100644 --- a/services/mam-api/src/routes/settings.js +++ b/services/mam-api/src/routes/settings.js @@ -2,11 +2,117 @@ import express from 'express'; import pool from '../db/pool.js'; import { requireAuth } from '../middleware/auth.js'; import { getAmppConfig } from '../ampp/client.js'; +import { rebuildS3Client, buildTestClient, testS3Connection, getS3Bucket } from '../s3/client.js'; const router = express.Router(); router.use(requireAuth); -// ── AMPP integration ─────────────────────────────────────────────────────── +// ── S3 / Object Storage ─────────────────────────────────────────────────────── + +router.get('/s3', async (req, res, next) => { + try { + const result = await pool.query( + `SELECT key, value FROM settings + WHERE key IN ('s3_endpoint','s3_bucket','s3_access_key','s3_secret_key','s3_region')` + ); + const out = { + s3_endpoint: '', + s3_bucket: getS3Bucket(), + s3_access_key: '', + s3_secret_key_exists: false, + s3_region: 'us-east-1', + }; + for (const { key, value } of result.rows) { + if (key === 's3_secret_key') { + out.s3_secret_key_exists = !!value; + } else { + out[key] = value || ''; + } + } + res.json(out); + } catch (err) { + next(err); + } +}); + +router.put('/s3', async (req, res, next) => { + try { + const { s3_endpoint, s3_bucket, s3_access_key, s3_secret_key, s3_region } = req.body; + + if (!s3_endpoint) return res.status(400).json({ error: 's3_endpoint is required' }); + if (!s3_bucket) return res.status(400).json({ error: 's3_bucket is required' }); + + const keys = { s3_endpoint, s3_bucket, s3_region: s3_region || 'us-east-1' }; + if (s3_access_key) keys.s3_access_key = s3_access_key; + if (s3_secret_key) keys.s3_secret_key = s3_secret_key; + + for (const [key, value] of Object.entries(keys)) { + await pool.query( + `INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW()) + ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()`, + [key, value.trim()] + ); + } + + // Fetch the full current config (including any previously saved secret) + const saved = await pool.query( + `SELECT key, value FROM settings + WHERE key IN ('s3_endpoint','s3_bucket','s3_access_key','s3_secret_key','s3_region') + AND value IS NOT NULL AND value <> ''` + ); + const cfg = {}; + for (const { key, value } of saved.rows) { + switch (key) { + case 's3_endpoint': cfg.endpoint = value; break; + case 's3_bucket': cfg.bucket = value; break; + case 's3_access_key': cfg.accessKey = value; break; + case 's3_secret_key': cfg.secretKey = value; break; + case 's3_region': cfg.region = value; break; + } + } + rebuildS3Client(cfg); + + res.json({ message: 'S3 settings saved and applied' }); + } catch (err) { + next(err); + } +}); + +// Test with the values the browser just typed (before saving), or with saved +// creds if the fields are left blank. Secret is sent only if user typed it. +router.post('/s3/test', async (req, res, next) => { + try { + const { s3_endpoint, s3_bucket, s3_access_key, s3_secret_key, s3_region } = req.body; + + // Merge submitted values with anything already saved in the DB + const saved = await pool.query( + `SELECT key, value FROM settings + WHERE key IN ('s3_endpoint','s3_bucket','s3_access_key','s3_secret_key','s3_region') + AND value IS NOT NULL AND value <> ''` + ); + const fromDb = {}; + for (const { key, value } of saved.rows) fromDb[key] = value; + + const cfg = { + endpoint: (s3_endpoint || fromDb.s3_endpoint || '').trim(), + bucket: (s3_bucket || fromDb.s3_bucket || getS3Bucket()).trim(), + accessKey: (s3_access_key || fromDb.s3_access_key || '').trim(), + secretKey: (s3_secret_key || fromDb.s3_secret_key || '').trim(), + region: (s3_region || fromDb.s3_region || 'us-east-1').trim(), + }; + + if (!cfg.endpoint) return res.status(400).json({ error: 'S3 endpoint is required' }); + if (!cfg.bucket) return res.status(400).json({ error: 'S3 bucket is required' }); + + const client = buildTestClient(cfg); + const result = await testS3Connection(client, cfg.bucket); + res.json(result); + } catch (err) { + res.status(400).json({ ok: false, error: err.message }); + } +}); + +// ── AMPP integration ─────────────────────────────────────────────────────────── router.get('/ampp', async (req, res, next) => { try { @@ -65,8 +171,7 @@ router.post('/ampp/test', async (req, res, next) => { } }); -// ── Hardware inventory ───────────────────────────────────────────────────── -// GET /api/v1/settings/hardware — aggregate GPU/BMD capabilities from all cluster nodes +// ── Hardware inventory ──────────────────────────────────────────────────────── router.get('/hardware', async (req, res, next) => { try { @@ -90,8 +195,7 @@ router.get('/hardware', async (req, res, next) => { } }); -// ── GPU/Transcoding settings ─────────────────────────────────────────────── -// Keys: gpu_transcode_enabled, gpu_codec, gpu_preset, gpu_bitrate_mbps, gpu_node +// ── GPU / Transcoding ───────────────────────────────────────────────────────── router.get('/transcoding', async (req, res, next) => { try { @@ -131,9 +235,7 @@ router.put('/transcoding', async (req, res, next) => { } }); -// ── Capture service routing ──────────────────────────────────────────────── -// capture_service_url — points to the remote capture node's API -// When set, the local capture proxy routes all requests to this URL instead of the sidecar +// ── Capture service routing ─────────────────────────────────────────────────── router.get('/capture-service', async (req, res, next) => { try {