dragonflight/services/mam-api/src/routes/settings.js

266 lines
9.4 KiB
JavaScript
Raw Normal View History

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);
// ── 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 {
const result = await pool.query(
"SELECT key, value FROM settings WHERE key IN ('ampp_base_url', 'ampp_token')"
);
const out = {};
for (const row of result.rows) {
if (row.key === 'ampp_token') {
out.ampp_token_exists = true;
} else {
out[row.key] = row.value;
}
}
res.json(out);
} catch (err) {
next(err);
}
});
router.put('/ampp', async (req, res, next) => {
try {
const { ampp_base_url, ampp_token } = req.body;
if (!ampp_base_url) return res.status(400).json({ error: 'ampp_base_url is required' });
const baseUrl = ampp_base_url.trim().replace(/\/$/, '');
await pool.query(
`INSERT INTO settings (key, value, updated_at) VALUES ('ampp_base_url', $1, NOW())
ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`,
[baseUrl]
);
if (ampp_token) {
await pool.query(
`INSERT INTO settings (key, value, updated_at) VALUES ('ampp_token', $1, NOW())
ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`,
[ampp_token.trim()]
);
}
res.json({ message: 'AMPP settings saved' });
} catch (err) {
next(err);
}
});
router.post('/ampp/test', async (req, res, next) => {
try {
const config = await getAmppConfig();
if (!config) return res.status(400).json({ error: 'AMPP credentials not configured' });
const testUrl = `${config.ampp_base_url}/api/v1/store/folder/folders?limit=1`;
const testRes = await fetch(testUrl, {
headers: { Authorization: `Bearer ${config.ampp_token}`, 'Content-Type': 'application/json' },
});
if (!testRes.ok) return res.status(400).json({ error: `AMPP returned HTTP ${testRes.status}` });
res.json({ message: 'AMPP connection successful' });
} catch (err) {
res.status(400).json({ error: `Connection failed: ${err.message}` });
}
});
// ── Hardware inventory ────────────────────────────────────────────────────────
router.get('/hardware', async (req, res, next) => {
try {
const result = await pool.query(
`SELECT id, hostname, role, ip_address, capabilities,
EXTRACT(EPOCH FROM (NOW() - last_seen))::int AS stale_seconds
FROM cluster_nodes
ORDER BY role, hostname`
);
const nodes = result.rows.map(n => ({
id: n.id,
hostname: n.hostname,
role: n.role,
ip_address: n.ip_address,
online: n.stale_seconds < 120,
capabilities: n.capabilities || {},
}));
res.json({ nodes });
} catch (err) {
next(err);
}
});
// ── GPU / Transcoding ─────────────────────────────────────────────────────────
router.get('/transcoding', async (req, res, next) => {
try {
const result = await pool.query(
`SELECT key, value FROM settings
WHERE key IN ('gpu_transcode_enabled','gpu_codec','gpu_preset','gpu_bitrate_mbps','gpu_node')`
);
const out = {
gpu_transcode_enabled: 'false',
gpu_codec: 'h264_nvenc',
gpu_preset: 'p4',
gpu_bitrate_mbps: '8',
gpu_node: '',
};
for (const { key, value } of result.rows) out[key] = value;
res.json(out);
} catch (err) {
next(err);
}
});
router.put('/transcoding', async (req, res, next) => {
try {
const allowed = ['gpu_transcode_enabled', 'gpu_codec', 'gpu_preset', 'gpu_bitrate_mbps', 'gpu_node'];
for (const key of allowed) {
if (req.body[key] !== undefined) {
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, String(req.body[key])]
);
}
}
res.json({ message: 'Transcoding settings saved' });
} catch (err) {
next(err);
}
});
// ── Capture service routing ───────────────────────────────────────────────────
router.get('/capture-service', async (req, res, next) => {
try {
const result = await pool.query(
"SELECT value FROM settings WHERE key = 'capture_service_url'"
);
res.json({ capture_service_url: result.rows[0]?.value || '' });
} catch (err) {
next(err);
}
});
router.put('/capture-service', async (req, res, next) => {
try {
const url = (req.body.capture_service_url || '').trim().replace(/\/$/, '');
await pool.query(
`INSERT INTO settings (key, value, updated_at) VALUES ('capture_service_url', $1, NOW())
ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`,
[url]
);
res.json({ message: 'Capture service URL saved' });
} catch (err) {
next(err);
}
});
export default router;