From fb1959fde8f19cdc20c609033cbd02f6b7f567e1 Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Wed, 28 Jan 2026 07:43:31 -0700 Subject: [PATCH 1/2] Add API endpoints for container management through Launchpad Introduces a new router providing RESTful API endpoints for managing containers, including create, read, update, and delete operations with API key authentication. The new endpoints are mounted at the top level in server.js to support automation and API clients. --- create-a-container/routers/api_containers.js | 107 +++++++++++++++++++ create-a-container/server.js | 4 + 2 files changed, 111 insertions(+) create mode 100644 create-a-container/routers/api_containers.js diff --git a/create-a-container/routers/api_containers.js b/create-a-container/routers/api_containers.js new file mode 100644 index 00000000..804af05f --- /dev/null +++ b/create-a-container/routers/api_containers.js @@ -0,0 +1,107 @@ +const express = require('express'); +const router = express.Router(); +const { Container, Node } = require('../models'); + +// Simple API key middleware (expects Bearer ) +function requireApiKey(req, res, next) { + const auth = req.get('authorization') || ''; + const parts = auth.split(' '); + if (parts.length === 2 && parts[0] === 'Bearer' && parts[1] === process.env.API_KEY) { + return next(); + } + return res.status(401).json({ error: 'Unauthorized' }); +} + +// GET /containers?hostname=foo +router.get('/containers', requireApiKey, async (req, res) => { + try { + const { hostname } = req.query; + if (!hostname) { + // Return empty array to keep client parsing simple + return res.json([]); + } + + const containers = await Container.findAll({ + where: { hostname }, + include: [{ model: Node, as: 'node', attributes: ['id', 'name'] }] + }); + + // Normalize to plain JSON + const out = containers.map(c => ({ + id: c.id, + hostname: c.hostname, + ipv4Address: c.ipv4Address, + macAddress: c.macAddress, + node: c.node ? { id: c.node.id, name: c.node.name } : null, + createdAt: c.createdAt, + updatedAt: c.updatedAt + })); + + return res.json(out); + } catch (err) { + console.error('API GET /containers error:', err); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + +// POST /containers - create a new container record (idempotent) +router.post('/containers', requireApiKey, async (req, res) => { + try { + const { hostname } = req.body; + if (!hostname) return res.status(400).json({ error: 'hostname required' }); + + let container = await Container.findOne({ where: { hostname } }); + if (container) { + return res.status(200).json({ containerId: container.id, message: 'Already exists' }); + } + + container = await Container.create({ + hostname, + username: req.body.username || 'api', + ipv4Address: req.body.ipv4Address || null, + macAddress: req.body.macAddress || null + }); + + return res.status(201).json({ containerId: container.id, message: 'Created' }); + } catch (err) { + console.error('API POST /containers error:', err); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + +// PUT /containers/:id - update container record +router.put('/containers/:id', requireApiKey, async (req, res) => { + try { + const id = parseInt(req.params.id, 10); + const container = await Container.findByPk(id); + if (!container) return res.status(404).json({ error: 'Not found' }); + + await container.update({ + ipv4Address: req.body.ipv4Address ?? container.ipv4Address, + macAddress: req.body.macAddress ?? container.macAddress, + osRelease: req.body.osRelease ?? container.osRelease + }); + + return res.status(200).json({ message: 'Updated' }); + } catch (err) { + console.error('API PUT /containers/:id error:', err); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + +// DELETE /containers/:id +router.delete('/containers/:id', requireApiKey, async (req, res) => { + try { + const id = parseInt(req.params.id, 10); + const container = await Container.findByPk(id); + if (!container) return res.status(404).json({ error: 'Not found' }); + + await container.destroy(); + return res.status(204).send(); + } catch (err) { + console.error('API DELETE /containers/:id error:', err); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router; diff --git a/create-a-container/server.js b/create-a-container/server.js index 31a40adb..2205c840 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -97,12 +97,16 @@ async function main() { }); // --- Mount Routers --- + // Mount top-level API endpoints (used by automation / API clients) + const apiContainersRouter = require('./routers/api_containers'); const loginRouter = require('./routers/login'); const registerRouter = require('./routers/register'); const usersRouter = require('./routers/users'); const groupsRouter = require('./routers/groups'); const sitesRouter = require('./routers/sites'); // Includes nested nodes and containers routers const jobsRouter = require('./routers/jobs'); + // expose API endpoints before HTML routes so they respond at top-level + app.use('/', apiContainersRouter); app.use('/jobs', jobsRouter); app.use('/login', loginRouter); app.use('/register', registerRouter); From 2431555b017f2f55cd53f5bfd92f549ae86951fd Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Wed, 28 Jan 2026 13:07:25 -0700 Subject: [PATCH 2/2] Integrate API container routes into containers.js Removed api_containers.js and merged its API logic into containers.js. Now, API clients can interact with containers using Bearer token authentication on the main containers routes, supporting JSON responses for GET, POST, PUT, and DELETE operations. This unifies container management for both web and API clients and simplifies route maintenance. --- create-a-container/routers/api_containers.js | 107 ------------------- create-a-container/routers/containers.js | 99 ++++++++++++++++- 2 files changed, 96 insertions(+), 110 deletions(-) delete mode 100644 create-a-container/routers/api_containers.js diff --git a/create-a-container/routers/api_containers.js b/create-a-container/routers/api_containers.js deleted file mode 100644 index 804af05f..00000000 --- a/create-a-container/routers/api_containers.js +++ /dev/null @@ -1,107 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const { Container, Node } = require('../models'); - -// Simple API key middleware (expects Bearer ) -function requireApiKey(req, res, next) { - const auth = req.get('authorization') || ''; - const parts = auth.split(' '); - if (parts.length === 2 && parts[0] === 'Bearer' && parts[1] === process.env.API_KEY) { - return next(); - } - return res.status(401).json({ error: 'Unauthorized' }); -} - -// GET /containers?hostname=foo -router.get('/containers', requireApiKey, async (req, res) => { - try { - const { hostname } = req.query; - if (!hostname) { - // Return empty array to keep client parsing simple - return res.json([]); - } - - const containers = await Container.findAll({ - where: { hostname }, - include: [{ model: Node, as: 'node', attributes: ['id', 'name'] }] - }); - - // Normalize to plain JSON - const out = containers.map(c => ({ - id: c.id, - hostname: c.hostname, - ipv4Address: c.ipv4Address, - macAddress: c.macAddress, - node: c.node ? { id: c.node.id, name: c.node.name } : null, - createdAt: c.createdAt, - updatedAt: c.updatedAt - })); - - return res.json(out); - } catch (err) { - console.error('API GET /containers error:', err); - return res.status(500).json({ error: 'Internal server error' }); - } -}); - -// POST /containers - create a new container record (idempotent) -router.post('/containers', requireApiKey, async (req, res) => { - try { - const { hostname } = req.body; - if (!hostname) return res.status(400).json({ error: 'hostname required' }); - - let container = await Container.findOne({ where: { hostname } }); - if (container) { - return res.status(200).json({ containerId: container.id, message: 'Already exists' }); - } - - container = await Container.create({ - hostname, - username: req.body.username || 'api', - ipv4Address: req.body.ipv4Address || null, - macAddress: req.body.macAddress || null - }); - - return res.status(201).json({ containerId: container.id, message: 'Created' }); - } catch (err) { - console.error('API POST /containers error:', err); - return res.status(500).json({ error: 'Internal server error' }); - } -}); - -// PUT /containers/:id - update container record -router.put('/containers/:id', requireApiKey, async (req, res) => { - try { - const id = parseInt(req.params.id, 10); - const container = await Container.findByPk(id); - if (!container) return res.status(404).json({ error: 'Not found' }); - - await container.update({ - ipv4Address: req.body.ipv4Address ?? container.ipv4Address, - macAddress: req.body.macAddress ?? container.macAddress, - osRelease: req.body.osRelease ?? container.osRelease - }); - - return res.status(200).json({ message: 'Updated' }); - } catch (err) { - console.error('API PUT /containers/:id error:', err); - return res.status(500).json({ error: 'Internal server error' }); - } -}); - -// DELETE /containers/:id -router.delete('/containers/:id', requireApiKey, async (req, res) => { - try { - const id = parseInt(req.params.id, 10); - const container = await Container.findByPk(id); - if (!container) return res.status(404).json({ error: 'Not found' }); - - await container.destroy(); - return res.status(204).send(); - } catch (err) { - console.error('API DELETE /containers/:id error:', err); - return res.status(500).json({ error: 'Internal server error' }); - } -}); - -module.exports = router; diff --git a/create-a-container/routers/containers.js b/create-a-container/routers/containers.js index a03006c5..748b807e 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -65,8 +65,44 @@ router.get('/new', requireAuth, async (req, res) => { }); }); +// Helper to detect API bearer requests +function isApiRequest(req) { + const auth = req.get('authorization') || ''; + const parts = auth.split(' '); + return parts.length === 2 && parts[0] === 'Bearer' && parts[1] === process.env.API_KEY; +} + // GET /sites/:siteId/containers - List all containers for the logged-in user in this site -router.get('/', requireAuth, async (req, res) => { +router.get('/', async (req, res) => { + // If called by API clients using Bearer token, return JSON instead of HTML + if (isApiRequest(req)) { + try { + const siteId = parseInt(req.params.siteId, 10); + const site = await Site.findByPk(siteId); + if (!site) return res.status(404).json([]); + + // Limit search to nodes within this site + const nodes = await Node.findAll({ where: { siteId }, attributes: ['id'] }); + const nodeIds = nodes.map(n => n.id); + + const { hostname } = req.query; + const where = {}; + if (hostname) where.hostname = hostname; + where.nodeId = nodeIds; + + const containers = await Container.findAll({ where, include: [{ association: 'node', attributes: ['id', 'name'] }] }); + const out = containers.map(c => ({ id: c.id, hostname: c.hostname, ipv4Address: c.ipv4Address, macAddress: c.macAddress, node: c.node ? { id: c.node.id, name: c.node.name } : null, createdAt: c.createdAt })); + return res.json(out); + } catch (err) { + console.error('API GET /sites/:siteId/containers error:', err); + return res.status(500).json({ error: 'Internal server error' }); + } + } + + // Browser path: require authentication and render HTML + await new Promise(resolve => requireAuth(req, res, resolve)); + if (res.headersSent) return; // requireAuth already handled redirect + const siteId = parseInt(req.params.siteId, 10); const site = await Site.findByPk(siteId); @@ -203,12 +239,41 @@ router.post('/', async (req, res) => { // Validate site exists const site = await Site.findByPk(siteId); if (!site) { + if (isApiRequest(req)) return res.status(404).json({ error: 'Site not found' }); req.flash('error', 'Site not found'); return res.redirect('/sites'); } // TODO: build the container async in a Job try { + // If API client (Bearer token), perform a lightweight create and return JSON + if (isApiRequest(req)) { + try { + const { hostname, ipv4Address, macAddress, nodeName, containerId } = req.body; + if (!hostname) return res.status(400).json({ error: 'hostname required' }); + // attempt to associate node if provided + let node = null; + if (nodeName) node = await Node.findOne({ where: { name: nodeName, siteId } }); + + let existing = await Container.findOne({ where: { hostname } }); + if (existing) return res.status(200).json({ containerId: existing.id, message: 'Already exists' }); + + const created = await Container.create({ + hostname, + username: req.body.username || 'api', + nodeId: node ? node.id : null, + containerId: containerId || null, + macAddress: macAddress || null, + ipv4Address: ipv4Address || null + }); + + return res.status(201).json({ containerId: created.id, message: 'Created' }); + } catch (err) { + console.error('API POST /sites/:siteId/containers error:', err); + return res.status(500).json({ error: 'Internal server error' }); + } + } + const { hostname, template, services } = req.body; const [ nodeName, templateVmid ] = template.split(','); const node = await Node.findOne({ where: { name: nodeName, siteId } }); @@ -378,9 +443,25 @@ router.post('/', async (req, res) => { }); // PUT /sites/:siteId/containers/:id - Update container services -router.put('/:id', requireAuth, async (req, res) => { +router.put('/:id', async (req, res) => { const siteId = parseInt(req.params.siteId, 10); const containerId = parseInt(req.params.id, 10); + // API clients may update container metadata via Bearer token + if (isApiRequest(req)) { + try { + const container = await Container.findByPk(containerId); + if (!container) return res.status(404).json({ error: 'Not found' }); + await container.update({ + ipv4Address: req.body.ipv4Address ?? container.ipv4Address, + macAddress: req.body.macAddress ?? container.macAddress, + osRelease: req.body.osRelease ?? container.osRelease + }); + return res.status(200).json({ message: 'Updated' }); + } catch (err) { + console.error('API PUT /sites/:siteId/containers/:id error:', err); + return res.status(500).json({ error: 'Internal server error' }); + } + } const site = await Site.findByPk(siteId); if (!site) { @@ -513,9 +594,21 @@ router.put('/:id', requireAuth, async (req, res) => { }); // DELETE /sites/:siteId/containers/:id - Delete a container -router.delete('/:id', requireAuth, async (req, res) => { +router.delete('/:id', async (req, res) => { const siteId = parseInt(req.params.siteId, 10); const containerId = parseInt(req.params.id, 10); + // If API request, perform lightweight delete and return JSON/204 + if (isApiRequest(req)) { + try { + const container = await Container.findByPk(containerId); + if (!container) return res.status(404).json({ error: 'Not found' }); + await container.destroy(); + return res.status(204).send(); + } catch (err) { + console.error('API DELETE /sites/:siteId/containers/:id error:', err); + return res.status(500).json({ error: 'Internal server error' }); + } + } // Validate site exists const site = await Site.findByPk(siteId);