From d42d0df91b59ab0253ff32aeac2e7aa7bd2f3216 Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Wed, 28 Jan 2026 07:43:31 -0700 Subject: [PATCH 01/16] 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 | 2 + 2 files changed, 109 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 6083c2f9..1b5dc74d 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -114,6 +114,8 @@ 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'); From 0be81bd97b1e61e8514911f4da2a3c7043f9554d Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Wed, 28 Jan 2026 13:07:25 -0700 Subject: [PATCH 02/16] 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 | 70 +++++++++++- 2 files changed, 67 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 43e3ca1f..55a9dccd 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -115,8 +115,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); @@ -450,9 +486,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) { @@ -638,9 +690,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); From 560c172f021696b8e779bdff0332492bb561becb Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Fri, 30 Jan 2026 09:37:35 -0700 Subject: [PATCH 03/16] Update API request detection logic Refactors the isApiRequest helper to detect API requests based on the Accept header instead of the Authorization header. This improves compatibility with clients expecting JSON responses. --- create-a-container/routers/containers.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/create-a-container/routers/containers.js b/create-a-container/routers/containers.js index 55a9dccd..2ca1e092 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -117,9 +117,8 @@ 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; + const accept = (req.get('accept') || '').toLowerCase(); + return accept.includes('application/json') || accept.includes('application/vnd.api+json'); } // GET /sites/:siteId/containers - List all containers for the logged-in user in this site From d9af1b92fe14629f74810e46020fee0445eff64f Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Fri, 30 Jan 2026 09:39:52 -0700 Subject: [PATCH 04/16] Remove unused apiContainersRouter import Deleted the import of apiContainersRouter from server.js as it was not being used. This helps clean up the code and remove unnecessary dependencies. --- create-a-container/server.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/create-a-container/server.js b/create-a-container/server.js index 1b5dc74d..6083c2f9 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -114,8 +114,6 @@ 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'); From f075d837541a86f2792b7e5366b0bba8384eb522 Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Wed, 4 Feb 2026 08:44:21 -0700 Subject: [PATCH 05/16] Update server.js --- create-a-container/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/create-a-container/server.js b/create-a-container/server.js index 6083c2f9..2d9ae201 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -114,6 +114,7 @@ async function main() { }); // --- Mount Routers --- + const apiContainersRouter = require('./routers/api_containers'); const loginRouter = require('./routers/login'); const registerRouter = require('./routers/register'); const usersRouter = require('./routers/users'); From 3bb1a58ba693fbf91c6093b83ae737c489a4bfac Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Wed, 4 Feb 2026 09:03:07 -0700 Subject: [PATCH 06/16] Update server.js --- create-a-container/server.js | 1 - 1 file changed, 1 deletion(-) diff --git a/create-a-container/server.js b/create-a-container/server.js index 2d9ae201..6083c2f9 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -114,7 +114,6 @@ async function main() { }); // --- Mount Routers --- - const apiContainersRouter = require('./routers/api_containers'); const loginRouter = require('./routers/login'); const registerRouter = require('./routers/register'); const usersRouter = require('./routers/users'); From e9334071b7e080e16aa44a79c63fc9ee5ffe4032 Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Wed, 4 Feb 2026 15:40:49 -0700 Subject: [PATCH 07/16] Fixed API Response Handling + Payload Mapping --- create-a-container/routers/containers.js | 144 +++++++++++++++++------ 1 file changed, 111 insertions(+), 33 deletions(-) diff --git a/create-a-container/routers/containers.js b/create-a-container/routers/containers.js index 2ca1e092..341781e2 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -10,13 +10,18 @@ const serviceMap = require('../data/services.json'); /** * Normalize a Docker image reference to full format: host/org/image:tag * Examples: - * nginx → docker.io/library/nginx:latest - * nginx:alpine → docker.io/library/nginx:alpine - * myorg/myapp → docker.io/myorg/myapp:latest - * myorg/myapp:v1 → docker.io/myorg/myapp:v1 - * ghcr.io/org/app:v1 → ghcr.io/org/app:v1 + * nginx → docker.io/library/nginx:latest + * nginx:alpine → docker.io/library/nginx:alpine + * myorg/myapp → docker.io/myorg/myapp:latest + * myorg/myapp:v1 → docker.io/myorg/myapp:v1 + * ghcr.io/org/app:v1 → ghcr.io/org/app:v1 */ function normalizeDockerRef(ref) { + // If this looks like a git URL (starts with http/https/git), return as is + if (ref.startsWith('http://') || ref.startsWith('https://') || ref.startsWith('git@')) { + return ref; + } + // Split off tag first let tag = 'latest'; let imagePart = ref; @@ -61,6 +66,12 @@ function normalizeDockerRef(ref) { return `${host}/${org}/${image}:${tag}`; } +// Helper to detect API bearer requests +function isApiRequest(req) { + const accept = (req.get('accept') || '').toLowerCase(); + return accept.includes('application/json') || accept.includes('application/vnd.api+json'); +} + // GET /sites/:siteId/containers/new - Display form for creating a new container router.get('/new', requireAuth, async (req, res) => { // verify site exists @@ -115,12 +126,6 @@ router.get('/new', requireAuth, async (req, res) => { }); }); -// Helper to detect API bearer requests -function isApiRequest(req) { - const accept = (req.get('accept') || '').toLowerCase(); - return accept.includes('application/json') || accept.includes('application/vnd.api+json'); -} - // GET /sites/:siteId/containers - List all containers for the logged-in user in this site router.get('/', async (req, res) => { // If called by API clients using Bearer token, return JSON instead of HTML @@ -286,10 +291,12 @@ router.get('/:id/edit', requireAuth, async (req, res) => { // POST /sites/:siteId/containers - Create a new container (async via job) router.post('/', async (req, res) => { const siteId = parseInt(req.params.siteId, 10); + const isApi = isApiRequest(req); // Validate site exists const site = await Site.findByPk(siteId); if (!site) { + if (isApi) return res.status(404).json({ error: 'Site not found' }); await req.flash('error', 'Site not found'); return res.redirect('/sites'); } @@ -297,8 +304,36 @@ router.post('/', async (req, res) => { const t = await sequelize.transaction(); try { - const { hostname, template, customTemplate, services, environmentVars, entrypoint } = req.body; + let { hostname, template, customTemplate, services, environmentVars, entrypoint, + // Extract specific API fields if they differ from form data + template_name, repository, branch + } = req.body; + + // Handle API-specific payload mapping + if (isApi) { + // If repository is provided, treat it as a custom build + if (repository) { + template = 'custom'; + customTemplate = repository; + + // Inject repo/branch into env vars so the build script can use them + if (!environmentVars) environmentVars = []; + // Ensure environmentVars is an array if it came in as something else + if (!Array.isArray(environmentVars)) environmentVars = []; + + environmentVars.push({ key: 'BUILD_REPOSITORY', value: repository }); + environmentVars.push({ key: 'BUILD_BRANCH', value: branch || 'master' }); + if (template_name) environmentVars.push({ key: 'TEMPLATE_NAME', value: template_name }); + } else if (template_name && !template) { + // Fallback: if only template_name provided, assume it's the template + template = template_name; + } + } + // Determine user (Session user or API fallback) + // Note: If using Bearer auth, ensure your middleware populates req.user or similar + const currentUser = req.session?.user || req.user?.username || 'api-user'; + // Convert environment variables array to JSON object let envVarsJson = null; if (environmentVars && Array.isArray(environmentVars)) { @@ -316,9 +351,9 @@ router.post('/', async (req, res) => { let nodeName, templateName, node; if (template === 'custom' || !template) { - // Custom Docker image - parse and normalize the reference + // Custom Docker image or Git Repo if (!customTemplate || customTemplate.trim() === '') { - throw new Error('Custom template image is required'); + throw new Error('Custom template image or repository URL is required'); } templateName = normalizeDockerRef(customTemplate.trim()); @@ -338,30 +373,53 @@ router.post('/', async (req, res) => { } } else { // Standard Proxmox template - const [ nodeNamePart, templateVmid ] = template.split(','); - nodeName = nodeNamePart; - node = await Node.findOne({ where: { name: nodeName, siteId } }); - - if (!node) { - throw new Error(`Node "${nodeName}" not found`); + // Check if format is "nodeName,vmid" (Form) or just "vmid" or "name" (API) + let templateVmid; + + if (template.includes(',')) { + const [ nodeNamePart, vmidPart ] = template.split(','); + nodeName = nodeNamePart; + templateVmid = vmidPart; + } else { + // If just a name is passed via API, we have to find a node that has it + // This is a naive implementation finding the first node with this template + // Ideal for API usage where you might not know the exact node + const allNodes = await Node.findAll({ where: { siteId }}); + for (const n of allNodes) { + const api = await n.api(); + const tpls = await api.getLxcTemplates(n.name); + const found = tpls.find(t => t.name === template || t.vmid.toString() === template); + if (found) { + node = n; + nodeName = n.name; + templateVmid = found.vmid; + templateName = found.name; + break; + } + } + if (!node) throw new Error(`Template "${template}" not found on any node in this site`); } - - // Get the template name from Proxmox - const client = await node.api(); - const templates = await client.getLxcTemplates(node.name); - const templateContainer = templates.find(t => t.vmid === parseInt(templateVmid, 10)); - - if (!templateContainer) { - throw new Error(`Template with VMID ${templateVmid} not found on node ${nodeName}`); + + if (!node) { + node = await Node.findOne({ where: { name: nodeName, siteId } }); + if (!node) throw new Error(`Node "${nodeName}" not found`); + + // Get the template name from Proxmox + const client = await node.api(); + const templates = await client.getLxcTemplates(node.name); + const templateContainer = templates.find(t => t.vmid === parseInt(templateVmid, 10)); + + if (!templateContainer) { + throw new Error(`Template with VMID ${templateVmid} not found on node ${nodeName}`); + } + templateName = templateContainer.name; } - - templateName = templateContainer.name; } // Create the container record in pending status (VMID allocated by job) const container = await Container.create({ hostname, - username: req.session.user, + username: currentUser, status: 'pending', template: templateName, nodeId: node.id, @@ -447,7 +505,7 @@ router.post('/', async (req, res) => { // Create the job to perform the actual container creation const job = await Job.create({ command: `node bin/create-container.js --container-id=${container.id}`, - createdBy: req.session.user, + createdBy: currentUser, status: 'pending' }, { transaction: t }); @@ -457,6 +515,19 @@ router.post('/', async (req, res) => { // Commit the transaction await t.commit(); + if (isApi) { + return res.status(202).json({ + message: 'Container creation initiated', + status: 'pending', + jobId: job.id, + container: { + id: container.id, + hostname: container.hostname, + status: 'pending' + } + }); + } + await req.flash('success', `Container "${hostname}" is being created. Check back shortly for status updates.`); return res.redirect(`/jobs/${job.id}`); } catch (err) { @@ -479,6 +550,13 @@ router.post('/', async (req, res) => { errorMessage += err.message; } + if (isApi) { + return res.status(400).json({ + error: errorMessage, + details: err.message + }); + } + await req.flash('error', errorMessage); return res.redirect(`/sites/${siteId}/containers/new`); } @@ -792,4 +870,4 @@ router.delete('/:id', async (req, res) => { return res.redirect(`/sites/${siteId}/containers`); }); -module.exports = router; +module.exports = router; \ No newline at end of file From 696be6c987b9f2cc843ff5e44b85460380992b77 Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Wed, 4 Feb 2026 15:47:33 -0700 Subject: [PATCH 08/16] Fix container URL duplication and syntax --- create-a-container/routers/containers.js | 438 +++++++---------------- 1 file changed, 123 insertions(+), 315 deletions(-) diff --git a/create-a-container/routers/containers.js b/create-a-container/routers/containers.js index 341781e2..8e5be9f3 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -9,27 +9,20 @@ const serviceMap = require('../data/services.json'); /** * Normalize a Docker image reference to full format: host/org/image:tag - * Examples: - * nginx → docker.io/library/nginx:latest - * nginx:alpine → docker.io/library/nginx:alpine - * myorg/myapp → docker.io/myorg/myapp:latest - * myorg/myapp:v1 → docker.io/myorg/myapp:v1 - * ghcr.io/org/app:v1 → ghcr.io/org/app:v1 */ function normalizeDockerRef(ref) { // If this looks like a git URL (starts with http/https/git), return as is + // (Though with the fix below, this shouldn't be called for git repos anymore) if (ref.startsWith('http://') || ref.startsWith('https://') || ref.startsWith('git@')) { return ref; } - // Split off tag first let tag = 'latest'; let imagePart = ref; const lastColon = ref.lastIndexOf(':'); if (lastColon !== -1) { const potentialTag = ref.substring(lastColon + 1); - // Make sure this isn't a port number in a registry URL (e.g., registry:5000/image) if (!potentialTag.includes('/')) { tag = potentialTag; imagePart = ref.substring(0, lastColon); @@ -43,21 +36,16 @@ function normalizeDockerRef(ref) { let image; if (parts.length === 1) { - // Just image name: nginx image = parts[0]; } else if (parts.length === 2) { - // Could be org/image or host/image - // If first part contains a dot or colon, it's a registry host if (parts[0].includes('.') || parts[0].includes(':')) { host = parts[0]; image = parts[1]; } else { - // org/image org = parts[0]; image = parts[1]; } } else { - // host/org/image or host/path/to/image host = parts[0]; image = parts[parts.length - 1]; org = parts.slice(1, -1).join('/'); @@ -72,9 +60,8 @@ function isApiRequest(req) { return accept.includes('application/json') || accept.includes('application/vnd.api+json'); } -// GET /sites/:siteId/containers/new - Display form for creating a new container +// GET /sites/:siteId/containers/new router.get('/new', requireAuth, async (req, res) => { - // verify site exists const siteId = parseInt(req.params.siteId, 10); const site = await Site.findByPk(siteId); if (!site) { @@ -82,7 +69,6 @@ router.get('/new', requireAuth, async (req, res) => { return res.redirect('/sites'); } - // Get valid container templates from all nodes in this site const templates = []; const nodes = await Node.findAll({ where: { @@ -95,12 +81,9 @@ router.get('/new', requireAuth, async (req, res) => { }, }); - // TODO: use datamodel backed templates instead of querying Proxmox here for (const node of nodes) { const client = await node.api(); - const lxcTemplates = await client.getLxcTemplates(node.name); - for (const lxc of lxcTemplates) { templates.push({ vmid: lxc.vmid, @@ -111,7 +94,6 @@ router.get('/new', requireAuth, async (req, res) => { } } - // Get external domains for this site const externalDomains = await ExternalDomain.findAll({ where: { siteId }, order: [['name', 'ASC']] @@ -121,21 +103,19 @@ router.get('/new', requireAuth, async (req, res) => { site, templates, externalDomains, - container: undefined, // Not editing + container: undefined, req }); }); -// GET /sites/:siteId/containers - List all containers for the logged-in user in this site +// GET /sites/:siteId/containers 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); @@ -153,26 +133,19 @@ router.get('/', async (req, res) => { } } - // Browser path: require authentication and render HTML await new Promise(resolve => requireAuth(req, res, resolve)); - if (res.headersSent) return; // requireAuth already handled redirect + if (res.headersSent) return; const siteId = parseInt(req.params.siteId, 10); - const site = await Site.findByPk(siteId); if (!site) { await req.flash('error', 'Site not found'); return res.redirect('/sites'); } - // Find all nodes that belong to this site - const nodes = await Node.findAll({ - where: { siteId }, - attributes: ['id'] - }); + const nodes = await Node.findAll({ where: { siteId }, attributes: ['id'] }); const nodeIds = nodes.map(n => n.id); - // Find containers that belong to nodes in this site and belong to the current user const containers = await Container.findAll({ where: { username: req.session.user, @@ -190,13 +163,10 @@ router.get('/', async (req, res) => { ] }); - // Map containers to view models const rows = containers.map(c => { const services = c.services || []; - // sshPort: externalPort of service with type transport, protocol tcp, and internalPort 22 const ssh = services.find(s => s.type === 'transport' && s.transportService?.protocol === 'tcp' && Number(s.internalPort) === 22); const sshPort = ssh?.transportService?.externalPort || null; - // httpPort: internalPort of first service type http const http = services.find(s => s.type === 'http'); const httpPort = http ? http.internalPort : null; return { @@ -212,14 +182,10 @@ router.get('/', async (req, res) => { }; }); - return res.render('containers/index', { - rows, - site, - req - }); + return res.render('containers/index', { rows, site, req }); }); -// GET /sites/:siteId/containers/:id/edit - Display form for editing container services +// GET /sites/:siteId/containers/:id/edit router.get('/:id/edit', requireAuth, async (req, res) => { const siteId = parseInt(req.params.siteId, 10); const containerId = parseInt(req.params.id, 10); @@ -230,38 +196,17 @@ router.get('/:id/edit', requireAuth, async (req, res) => { return res.redirect('/sites'); } - // Find the container with ownership check const container = await Container.findOne({ - where: { - id: containerId, - username: req.session.user - }, + where: { id: containerId, username: req.session.user }, include: [ - { - model: Node, - as: 'node', - where: { siteId } - }, + { model: Node, as: 'node', where: { siteId } }, { model: Service, as: 'services', include: [ - { - model: HTTPService, - as: 'httpService', - include: [{ - model: ExternalDomain, - as: 'externalDomain' - }] - }, - { - model: TransportService, - as: 'transportService' - }, - { - model: DnsService, - as: 'dnsService' - } + { model: HTTPService, as: 'httpService', include: [{ model: ExternalDomain, as: 'externalDomain' }] }, + { model: TransportService, as: 'transportService' }, + { model: DnsService, as: 'dnsService' } ] } ] @@ -272,28 +217,23 @@ router.get('/:id/edit', requireAuth, async (req, res) => { return res.redirect(`/sites/${siteId}/containers`); } - // Get external domains for this site - const externalDomains = await ExternalDomain.findAll({ - where: { siteId }, - order: [['name', 'ASC']] - }); + const externalDomains = await ExternalDomain.findAll({ where: { siteId }, order: [['name', 'ASC']] }); return res.render('containers/form', { site, container, externalDomains, - templates: [], // Not needed for edit + templates: [], isEdit: true, req }); }); -// POST /sites/:siteId/containers - Create a new container (async via job) +// POST /sites/:siteId/containers router.post('/', async (req, res) => { const siteId = parseInt(req.params.siteId, 10); const isApi = isApiRequest(req); - // Validate site exists const site = await Site.findByPk(siteId); if (!site) { if (isApi) return res.status(404).json({ error: 'Site not found' }); @@ -305,36 +245,41 @@ router.post('/', async (req, res) => { try { let { hostname, template, customTemplate, services, environmentVars, entrypoint, - // Extract specific API fields if they differ from form data + // Extract specific API fields template_name, repository, branch } = req.body; - // Handle API-specific payload mapping + // --- API Payload Mapping --- if (isApi) { - // If repository is provided, treat it as a custom build if (repository) { - template = 'custom'; - customTemplate = repository; - - // Inject repo/branch into env vars so the build script can use them + // Source Build Scenario: + // We do NOT set template='custom'. We must use the base template_name provided. + // The repository is passed ONLY via environment variables. + + if (template_name) { + template = template_name; + // We deliberately leave 'customTemplate' undefined so it triggers the standard LXC lookup logic below. + } else { + throw new Error('When providing a repository, you must also provide a template_name (e.g., "debian-template") for the base container.'); + } + + // Inject repo/branch into env vars if (!environmentVars) environmentVars = []; // Ensure environmentVars is an array if it came in as something else if (!Array.isArray(environmentVars)) environmentVars = []; environmentVars.push({ key: 'BUILD_REPOSITORY', value: repository }); environmentVars.push({ key: 'BUILD_BRANCH', value: branch || 'master' }); - if (template_name) environmentVars.push({ key: 'TEMPLATE_NAME', value: template_name }); + } else if (template_name && !template) { - // Fallback: if only template_name provided, assume it's the template + // Fallback: if only template_name provided (no repo), assume it's the template template = template_name; } } + // --------------------------- - // Determine user (Session user or API fallback) - // Note: If using Bearer auth, ensure your middleware populates req.user or similar const currentUser = req.session?.user || req.user?.username || 'api-user'; - // Convert environment variables array to JSON object let envVarsJson = null; if (environmentVars && Array.isArray(environmentVars)) { const envObj = {}; @@ -350,15 +295,15 @@ router.post('/', async (req, res) => { let nodeName, templateName, node; - if (template === 'custom' || !template) { - // Custom Docker image or Git Repo + // LOGIC: Custom (Docker) vs Standard (LXC) + if (template === 'custom' || (!template && customTemplate)) { + // Custom Docker image if (!customTemplate || customTemplate.trim() === '') { - throw new Error('Custom template image or repository URL is required'); + throw new Error('Custom template image is required'); } templateName = normalizeDockerRef(customTemplate.trim()); - // For custom templates, pick the first available node in the site node = await Node.findOne({ where: { siteId, @@ -372,39 +317,56 @@ router.post('/', async (req, res) => { throw new Error('No nodes with API access available in this site'); } } else { - // Standard Proxmox template - // Check if format is "nodeName,vmid" (Form) or just "vmid" or "name" (API) + // Standard Proxmox template (LXC) let templateVmid; - if (template.includes(',')) { + if (template && template.includes(',')) { + // Form submitted "nodeName,vmid" const [ nodeNamePart, vmidPart ] = template.split(','); nodeName = nodeNamePart; templateVmid = vmidPart; } else { - // If just a name is passed via API, we have to find a node that has it - // This is a naive implementation finding the first node with this template - // Ideal for API usage where you might not know the exact node - const allNodes = await Node.findAll({ where: { siteId }}); + // API submitted just the name "debian-template" + // Find a node that has this template + const allNodes = await Node.findAll({ + where: { siteId }, + // Filter for nodes that are actually online/configured + attributes: ['id', 'name', 'apiUrl', 'tokenId', 'secret'] + }); + + let foundTemplate = null; + for (const n of allNodes) { - const api = await n.api(); - const tpls = await api.getLxcTemplates(n.name); - const found = tpls.find(t => t.name === template || t.vmid.toString() === template); - if (found) { - node = n; - nodeName = n.name; - templateVmid = found.vmid; - templateName = found.name; - break; + // Skip nodes without config + if (!n.apiUrl || !n.tokenId) continue; + + try { + const api = await n.api(); + const tpls = await api.getLxcTemplates(n.name); + // Match by name or stringified vmid + const found = tpls.find(t => t.name === template || t.vmid.toString() === template); + if (found) { + node = n; + nodeName = n.name; + templateVmid = found.vmid; + foundTemplate = found; + break; + } + } catch (e) { + console.warn(`Failed to query templates from node ${n.name}:`, e.message); + continue; } } + if (!node) throw new Error(`Template "${template}" not found on any node in this site`); + templateName = foundTemplate.name; // Use the real name from Proxmox } - if (!node) { + // If we found the node via "nodeName,vmid" logic but haven't fetched details yet + if (!templateName) { node = await Node.findOne({ where: { name: nodeName, siteId } }); if (!node) throw new Error(`Node "${nodeName}" not found`); - // Get the template name from Proxmox const client = await node.api(); const templates = await client.getLxcTemplates(node.name); const templateContainer = templates.find(t => t.vmid === parseInt(templateVmid, 10)); @@ -416,12 +378,12 @@ router.post('/', async (req, res) => { } } - // Create the container record in pending status (VMID allocated by job) + // Create container record const container = await Container.create({ hostname, username: currentUser, status: 'pending', - template: templateName, + template: templateName, // Should now be "debian-12-standard..." or similar nodeId: node.id, containerId: null, macAddress: null, @@ -430,16 +392,14 @@ router.post('/', async (req, res) => { entrypoint: entrypoint && entrypoint.trim() ? entrypoint.trim() : null }, { transaction: t }); - // Create services if provided (validate within transaction) + // Services creation if (services && typeof services === 'object') { for (const key in services) { const service = services[key]; const { type, internalPort, externalHostname, externalDomainId, dnsName } = service; - // Validate required fields if (!type || !internalPort) continue; - // Determine the service type (http, transport, or dns) let serviceType; let protocol = null; @@ -448,7 +408,6 @@ router.post('/', async (req, res) => { } else if (type === 'srv') { serviceType = 'dns'; } else { - // tcp or udp serviceType = 'transport'; protocol = type; } @@ -459,40 +418,28 @@ router.post('/', async (req, res) => { internalPort: parseInt(internalPort, 10) }; - // Create the base service const createdService = await Service.create(serviceData, { transaction: t }); if (serviceType === 'http') { - // Validate that both hostname and domain are set - if (!externalHostname || !externalDomainId || externalDomainId === '') { + if (!externalHostname || !externalDomainId) { throw new Error('HTTP services must have both an external hostname and external domain'); } - - // Create HTTPService entry await HTTPService.create({ serviceId: createdService.id, externalHostname, externalDomainId: parseInt(externalDomainId, 10) }, { transaction: t }); } else if (serviceType === 'dns') { - // Validate DNS name is set - if (!dnsName) { - throw new Error('DNS services must have a DNS name'); - } - - // Create DnsService entry + if (!dnsName) throw new Error('DNS services must have a DNS name'); await DnsService.create({ serviceId: createdService.id, recordType: 'SRV', dnsName }, { transaction: t }); } else { - // For TCP/UDP services, auto-assign external port const minPort = 2000; const maxPort = 65565; const externalPort = await TransportService.nextAvailablePortInRange(protocol, minPort, maxPort, t); - - // Create TransportService entry await TransportService.create({ serviceId: createdService.id, protocol: protocol, @@ -502,17 +449,14 @@ router.post('/', async (req, res) => { } } - // Create the job to perform the actual container creation + // Create job const job = await Job.create({ command: `node bin/create-container.js --container-id=${container.id}`, createdBy: currentUser, status: 'pending' }, { transaction: t }); - // Link the container to the job await container.update({ creationJobId: job.id }, { transaction: t }); - - // Commit the transaction await t.commit(); if (isApi) { @@ -528,33 +472,21 @@ router.post('/', async (req, res) => { }); } - await req.flash('success', `Container "${hostname}" is being created. Check back shortly for status updates.`); + await req.flash('success', `Container "${hostname}" is being created.`); return res.redirect(`/jobs/${job.id}`); } catch (err) { - // Rollback the transaction await t.rollback(); - console.error('Error creating container:', err); - // Handle axios errors with detailed messages let errorMessage = 'Failed to create container: '; - if (err.response?.data) { - if (err.response.data.errors) { - errorMessage += JSON.stringify(err.response.data.errors); - } else if (err.response.data.message) { + if (err.response?.data?.message) { errorMessage += err.response.data.message; - } else { - errorMessage += err.message; - } } else { - errorMessage += err.message; + errorMessage += err.message; } if (isApi) { - return res.status(400).json({ - error: errorMessage, - details: err.message - }); + return res.status(400).json({ error: errorMessage, details: err.message }); } await req.flash('error', errorMessage); @@ -562,11 +494,11 @@ router.post('/', async (req, res) => { } }); -// PUT /sites/:siteId/containers/:id - Update container services +// PUT /sites/:siteId/containers/:id 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); @@ -578,31 +510,18 @@ router.put('/:id', async (req, res) => { }); return res.status(200).json({ message: 'Updated' }); } catch (err) { - console.error('API PUT /sites/:siteId/containers/:id error:', err); + console.error('API PUT Error:', err); return res.status(500).json({ error: 'Internal server error' }); } } const site = await Site.findByPk(siteId); - if (!site) { - await req.flash('error', 'Site not found'); - return res.redirect('/sites'); - } + if (!site) return res.redirect('/sites'); try { - // Find the container with ownership check const container = await Container.findOne({ - where: { - id: containerId, - username: req.session.user - }, - include: [ - { - model: Node, - as: 'node', - where: { siteId } - } - ] + where: { id: containerId, username: req.session.user }, + include: [{ model: Node, as: 'node', where: { siteId } }] }); if (!container) { @@ -611,19 +530,14 @@ router.put('/:id', async (req, res) => { } const { services, environmentVars, entrypoint } = req.body; - - // Check if this is a restart-only request (no config changes) const forceRestart = req.body.restart === 'true'; const isRestartOnly = forceRestart && !services && !environmentVars && entrypoint === undefined; - // Convert environment variables array to JSON object - let envVarsJson = container.environmentVars; // Default to existing + let envVarsJson = container.environmentVars; if (!isRestartOnly && environmentVars && Array.isArray(environmentVars)) { const envObj = {}; for (const env of environmentVars) { - if (env.key && env.key.trim()) { - envObj[env.key.trim()] = env.value || ''; - } + if (env.key) envObj[env.key.trim()] = env.value || ''; } envVarsJson = Object.keys(envObj).length > 0 ? JSON.stringify(envObj) : null; } else if (!isRestartOnly && !environmentVars) { @@ -633,15 +547,12 @@ router.put('/:id', async (req, res) => { const newEntrypoint = isRestartOnly ? container.entrypoint : (entrypoint && entrypoint.trim() ? entrypoint.trim() : null); - // Check if env vars or entrypoint changed const envChanged = !isRestartOnly && container.environmentVars !== envVarsJson; const entrypointChanged = !isRestartOnly && container.entrypoint !== newEntrypoint; const needsRestart = forceRestart || envChanged || entrypointChanged; - // Wrap all database operations in a transaction let restartJob = null; await sequelize.transaction(async (t) => { - // Update environment variables and entrypoint if changed if (envChanged || entrypointChanged) { await container.update({ environmentVars: envVarsJson, @@ -649,11 +560,9 @@ router.put('/:id', async (req, res) => { status: needsRestart && container.containerId ? 'restarting' : container.status }, { transaction: t }); } else if (forceRestart && container.containerId) { - // Just update status for force restart await container.update({ status: 'restarting' }, { transaction: t }); } - // Create restart job if needed and container has a VMID if (needsRestart && container.containerId) { restartJob = await Job.create({ command: `node bin/reconfigure-container.js --container-id=${container.id}`, @@ -662,13 +571,10 @@ router.put('/:id', async (req, res) => { }, { transaction: t }); } - // Process services in two phases: delete first, then create new if (services && typeof services === 'object') { - // Phase 1: Delete marked services + // Delete services marked for deletion for (const key in services) { - const service = services[key]; - const { id, deleted } = service; - + const { id, deleted } = services[key]; if (deleted === 'true' && id) { await Service.destroy({ where: { id: parseInt(id, 10), containerId: container.id }, @@ -676,77 +582,27 @@ router.put('/:id', async (req, res) => { }); } } - - // Phase 2: Create new services (those without an id or not marked as deleted) + // Create new services for (const key in services) { - const service = services[key]; - const { id, deleted, type, internalPort, externalHostname, externalDomainId, dnsName } = service; - - // Skip if marked as deleted or if it's an existing service (has id) - if (deleted === 'true' || id) continue; - - // Validate required fields - if (!type || !internalPort) continue; + const { id, deleted, type, internalPort, externalHostname, externalDomainId, dnsName } = services[key]; + if (deleted === 'true' || id || !type || !internalPort) continue; - // Determine the service type (http, transport, or dns) - let serviceType; - let protocol = null; - - if (type === 'http') { - serviceType = 'http'; - } else if (type === 'srv') { - serviceType = 'dns'; - } else { - // tcp or udp - serviceType = 'transport'; - protocol = type; - } - - const serviceData = { - containerId: container.id, - type: serviceType, - internalPort: parseInt(internalPort, 10) - }; + let serviceType = type === 'srv' ? 'dns' : (type === 'http' ? 'http' : 'transport'); + const protocol = (serviceType === 'transport') ? type : null; - // Create new service - const createdService = await Service.create(serviceData, { transaction: t }); + const createdService = await Service.create({ + containerId: container.id, + type: serviceType, + internalPort: parseInt(internalPort, 10) + }, { transaction: t }); if (serviceType === 'http') { - // Validate that both hostname and domain are set - if (!externalHostname || !externalDomainId || externalDomainId === '') { - throw new Error('HTTP services must have both an external hostname and external domain'); - } - - // Create HTTPService entry - await HTTPService.create({ - serviceId: createdService.id, - externalHostname, - externalDomainId: parseInt(externalDomainId, 10) - }, { transaction: t }); + await HTTPService.create({ serviceId: createdService.id, externalHostname, externalDomainId }, { transaction: t }); } else if (serviceType === 'dns') { - // Validate DNS name is set - if (!dnsName) { - throw new Error('DNS services must have a DNS name'); - } - - // Create DnsService entry - await DnsService.create({ - serviceId: createdService.id, - recordType: 'SRV', - dnsName - }, { transaction: t }); + await DnsService.create({ serviceId: createdService.id, recordType: 'SRV', dnsName }, { transaction: t }); } else { - // For TCP/UDP services, auto-assign external port - const minPort = 2000; - const maxPort = 65565; - const externalPort = await TransportService.nextAvailablePortInRange(protocol, minPort, maxPort); - - // Create TransportService entry - await TransportService.create({ - serviceId: createdService.id, - protocol: protocol, - externalPort - }, { transaction: t }); + const externalPort = await TransportService.nextAvailablePortInRange(protocol, 2000, 65565); + await TransportService.create({ serviceId: createdService.id, protocol, externalPort }, { transaction: t }); } } } @@ -766,107 +622,59 @@ router.put('/:id', async (req, res) => { } }); -// DELETE /sites/:siteId/containers/:id - Delete a container +// DELETE /sites/:siteId/containers/:id 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(); + await container.destroy(); // Triggers hooks/cascades return res.status(204).send(); } catch (err) { - console.error('API DELETE /sites/:siteId/containers/:id error:', err); + console.error('API DELETE Error:', err); return res.status(500).json({ error: 'Internal server error' }); } } - // Validate site exists const site = await Site.findByPk(siteId); - if (!site) { - await req.flash('error', 'Site not found'); - return res.redirect('/sites'); - } + if (!site) return res.redirect('/sites'); - // Find the container with ownership check in query to prevent information leakage const container = await Container.findOne({ - where: { - id: containerId, - username: req.session.user - }, - include: [{ - model: Node, - as: 'node', - attributes: ['id', 'name', 'apiUrl', 'tokenId', 'secret', 'tlsVerify', 'siteId'] - }] + where: { id: containerId, username: req.session.user }, + include: [{ model: Node, as: 'node' }] }); - if (!container) { - await req.flash('error', 'Container not found'); - return res.redirect(`/sites/${siteId}/containers`); - } - - // Verify the container's node belongs to this site - if (!container.node || container.node.siteId !== siteId) { - await req.flash('error', 'Container does not belong to this site'); + if (!container || !container.node || container.node.siteId !== siteId) { + await req.flash('error', 'Container not found or access denied'); return res.redirect(`/sites/${siteId}/containers`); } const node = container.node; - if (!node.apiUrl) { - await req.flash('error', 'Node API URL not configured'); - return res.redirect(`/sites/${siteId}/containers`); - } - - if (!node.tokenId || !node.secret) { - await req.flash('error', 'Node API token not configured'); - return res.redirect(`/sites/${siteId}/containers`); - } - try { - // Only attempt Proxmox deletion if containerId exists - if (container.containerId) { + if (container.containerId && node.apiUrl && node.tokenId) { const api = await node.api(); - - // Sanity check: verify the container in Proxmox matches our database record try { - const proxmoxConfig = await api.lxcConfig(node.name, container.containerId); - const proxmoxHostname = proxmoxConfig.hostname; - - if (proxmoxHostname && proxmoxHostname !== container.hostname) { - console.error(`Hostname mismatch: DB has "${container.hostname}", Proxmox has "${proxmoxHostname}" for VMID ${container.containerId}`); - await req.flash('error', `Safety check failed: Proxmox container hostname "${proxmoxHostname}" does not match database hostname "${container.hostname}". Manual intervention required.`); - return res.redirect(`/sites/${siteId}/containers`); + const config = await api.lxcConfig(node.name, container.containerId); + if (config.hostname && config.hostname !== container.hostname) { + await req.flash('error', `Hostname mismatch (DB: ${container.hostname} vs Proxmox: ${config.hostname}). Delete aborted.`); + return res.redirect(`/sites/${siteId}/containers`); } - - // Delete from Proxmox await api.deleteContainer(node.name, container.containerId, true, true); - console.log(`Deleted container ${container.containerId} from Proxmox node ${node.name}`); } catch (proxmoxError) { - // If container doesn't exist in Proxmox (404 or similar), continue with DB deletion - if (proxmoxError.response?.status === 500 && proxmoxError.response?.data?.errors?.vmid) { - console.log(`Container ${container.containerId} not found in Proxmox, proceeding with DB deletion`); - } else if (proxmoxError.response?.status === 404) { - console.log(`Container ${container.containerId} not found in Proxmox, proceeding with DB deletion`); - } else { - throw proxmoxError; - } + console.log(`Proxmox deletion skipped or failed: ${proxmoxError.message}`); } - } else { - console.log(`Container ${container.hostname} has no containerId, skipping Proxmox deletion`); } - - // Delete from database (cascade deletes associated services) await container.destroy(); } catch (error) { console.error(error); - await req.flash('error', `Failed to delete container: ${error.message}`); + await req.flash('error', `Failed to delete: ${error.message}`); return res.redirect(`/sites/${siteId}/containers`); } - await req.flash('success', `Container ${container.hostname} deleted successfully`); + await req.flash('success', 'Container deleted successfully'); return res.redirect(`/sites/${siteId}/containers`); }); From 8b0db76b963909054a4650cc289ec5c62803c1ea Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Wed, 4 Feb 2026 16:43:25 -0700 Subject: [PATCH 09/16] Add API template name GET request for troubleshooting --- create-a-container/routers/containers.js | 42 +++++++++++++++++------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/create-a-container/routers/containers.js b/create-a-container/routers/containers.js index 8e5be9f3..f2ea9aee 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -60,15 +60,20 @@ function isApiRequest(req) { return accept.includes('application/json') || accept.includes('application/vnd.api+json'); } -// GET /sites/:siteId/containers/new +// GET /sites/:siteId/containers/new - List available templates and options router.get('/new', requireAuth, async (req, res) => { const siteId = parseInt(req.params.siteId, 10); + const isApi = isApiRequest(req); + + // verify site exists const site = await Site.findByPk(siteId); if (!site) { + if (isApi) return res.status(404).json({ error: 'Site not found' }); await req.flash('error', 'Site not found'); return res.redirect('/sites'); } + // Get valid container templates from all nodes in this site const templates = []; const nodes = await Node.findAll({ where: { @@ -82,28 +87,43 @@ router.get('/new', requireAuth, async (req, res) => { }); for (const node of nodes) { - const client = await node.api(); - const lxcTemplates = await client.getLxcTemplates(node.name); - for (const lxc of lxcTemplates) { - templates.push({ - vmid: lxc.vmid, - name: lxc.name, - status: lxc.status, - node: node.name - }); + try { + const client = await node.api(); + const lxcTemplates = await client.getLxcTemplates(node.name); + + for (const lxc of lxcTemplates) { + templates.push({ + // The wrapper likely maps Proxmox 'volid' to 'name' or similar + name: lxc.name || lxc.volid, + size: lxc.size, + node: node.name + }); + } + } catch (err) { + console.error(`Error fetching templates from node ${node.name}:`, err.message); } } + // Get external domains for this site const externalDomains = await ExternalDomain.findAll({ where: { siteId }, order: [['name', 'ASC']] }); + if (isApi) { + return res.json({ + site_id: site.id, + templates: templates, + domains: externalDomains + }); + } + // ---------------------- + return res.render('containers/form', { site, templates, externalDomains, - container: undefined, + container: undefined, // Not editing req }); }); From 8fce3ec3098304ce3efdadc91e8a89bc0c7a1e05 Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Wed, 4 Feb 2026 16:47:06 -0700 Subject: [PATCH 10/16] Update containers.js --- create-a-container/routers/containers.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/create-a-container/routers/containers.js b/create-a-container/routers/containers.js index f2ea9aee..ce6ef128 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -60,10 +60,11 @@ function isApiRequest(req) { return accept.includes('application/json') || accept.includes('application/vnd.api+json'); } -// GET /sites/:siteId/containers/new - List available templates and options +// GET /sites/:siteId/containers/new - List available templates via API or HTML form router.get('/new', requireAuth, async (req, res) => { const siteId = parseInt(req.params.siteId, 10); - const isApi = isApiRequest(req); + // Check if this is an API request (requires the helper function defined earlier) + const isApi = isApiRequest(req); // verify site exists const site = await Site.findByPk(siteId); @@ -93,8 +94,9 @@ router.get('/new', requireAuth, async (req, res) => { for (const lxc of lxcTemplates) { templates.push({ - // The wrapper likely maps Proxmox 'volid' to 'name' or similar + // Proxmox usually returns 'name' (filename) or 'volid' name: lxc.name || lxc.volid, + vmid: lxc.vmid, size: lxc.size, node: node.name }); @@ -117,13 +119,13 @@ router.get('/new', requireAuth, async (req, res) => { domains: externalDomains }); } - // ---------------------- + // ---------------------------- return res.render('containers/form', { site, templates, externalDomains, - container: undefined, // Not editing + container: undefined, req }); }); From 75bbe1767adb0090f4ccc51c42437b1d27c1bc61 Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Fri, 6 Feb 2026 08:29:36 -0700 Subject: [PATCH 11/16] Migrate new utilities, fix security issues Removed isApiRequest from this file and imported it from ../utils/http-utils Applied requireAuth to the GET / route. Refactored GET / to unify the logic. It now runs the same database query for both API and HTML clients, calculating the service ports and statuses, and then switches the output format at the very end. Consolidated the 404 Site Not Found logic in GET /. --- create-a-container/routers/containers.js | 74 +++++++++++------------- create-a-container/utils/http.js | 14 +++++ 2 files changed, 48 insertions(+), 40 deletions(-) create mode 100644 create-a-container/utils/http.js diff --git a/create-a-container/routers/containers.js b/create-a-container/routers/containers.js index ce6ef128..a3bfc71d 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -6,13 +6,14 @@ const { Container, Service, HTTPService, TransportService, DnsService, Node, Sit const { requireAuth } = require('../middlewares'); const ProxmoxApi = require('../utils/proxmox-api'); const serviceMap = require('../data/services.json'); +// Imported from utils as requested +const { isApiRequest } = require('../utils/http-utils'); /** * Normalize a Docker image reference to full format: host/org/image:tag */ function normalizeDockerRef(ref) { // If this looks like a git URL (starts with http/https/git), return as is - // (Though with the fix below, this shouldn't be called for git repos anymore) if (ref.startsWith('http://') || ref.startsWith('https://') || ref.startsWith('git@')) { return ref; } @@ -54,16 +55,9 @@ function normalizeDockerRef(ref) { return `${host}/${org}/${image}:${tag}`; } -// Helper to detect API bearer requests -function isApiRequest(req) { - const accept = (req.get('accept') || '').toLowerCase(); - return accept.includes('application/json') || accept.includes('application/vnd.api+json'); -} - // GET /sites/:siteId/containers/new - List available templates via API or HTML form router.get('/new', requireAuth, async (req, res) => { const siteId = parseInt(req.params.siteId, 10); - // Check if this is an API request (requires the helper function defined earlier) const isApi = isApiRequest(req); // verify site exists @@ -131,36 +125,16 @@ router.get('/new', requireAuth, async (req, res) => { }); // GET /sites/:siteId/containers -router.get('/', async (req, res) => { - if (isApiRequest(req)) { - try { - const siteId = parseInt(req.params.siteId, 10); - const site = await Site.findByPk(siteId); - if (!site) return res.status(404).json([]); - - 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' }); - } - } - - await new Promise(resolve => requireAuth(req, res, resolve)); - if (res.headersSent) return; - +// Added requireAuth to ensure API keys and Sessions are validated +router.get('/', requireAuth, async (req, res) => { const siteId = parseInt(req.params.siteId, 10); const site = await Site.findByPk(siteId); + + // Unified Error Handling for Site 404 if (!site) { + if (isApiRequest(req)) { + return res.status(404).json({ error: 'Site not found' }); + } await req.flash('error', 'Site not found'); return res.redirect('/sites'); } @@ -168,11 +142,22 @@ router.get('/', async (req, res) => { const nodes = await Node.findAll({ where: { siteId }, attributes: ['id'] }); const nodeIds = nodes.map(n => n.id); + // Determine current user (support Session or API Token via requireAuth) + const currentUser = req.session?.user || req.user?.username; + + // Build query + const whereClause = { + username: currentUser, + nodeId: nodeIds + }; + + // Support hostname filtering for API requests if provided + if (req.query.hostname) { + whereClause.hostname = req.query.hostname; + } + const containers = await Container.findAll({ - where: { - username: req.session.user, - nodeId: nodeIds - }, + where: whereClause, include: [ { association: 'services', @@ -191,19 +176,28 @@ router.get('/', async (req, res) => { const sshPort = ssh?.transportService?.externalPort || null; const http = services.find(s => s.type === 'http'); const httpPort = http ? http.internalPort : null; + + // Common object structure for both API and View return { id: c.id, hostname: c.hostname, ipv4Address: c.ipv4Address, + // API might want raw MacAddress, View might not need it, but including it doesn't hurt + macAddress: c.macAddress, status: c.status, template: c.template, creationJobId: c.creationJobId, sshPort, httpPort, - nodeName: c.node ? c.node.name : '-' + nodeName: c.node ? c.node.name : '-', + createdAt: c.createdAt }; }); + if (isApiRequest(req)) { + return res.json({ containers: rows }); + } + return res.render('containers/index', { rows, site, req }); }); diff --git a/create-a-container/utils/http.js b/create-a-container/utils/http.js new file mode 100644 index 00000000..ec5453c5 --- /dev/null +++ b/create-a-container/utils/http.js @@ -0,0 +1,14 @@ +/** + * Helper to detect if a request is an API request based on headers. + * Checks if the client accepts JSON. + * * @param {import('express').Request} req + * @returns {boolean} + */ +function isApiRequest(req) { + const accept = (req.get('accept') || '').toLowerCase(); + return accept.includes('application/json') || accept.includes('application/vnd.api+json'); +} + +module.exports = { + isApiRequest +}; \ No newline at end of file From 70fb6ce74630d73e4c077d2f1e809cc9431b36b1 Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Fri, 6 Feb 2026 10:11:42 -0700 Subject: [PATCH 12/16] Fix wrong module call --- create-a-container/routers/containers.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/create-a-container/routers/containers.js b/create-a-container/routers/containers.js index a3bfc71d..1da39083 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -6,8 +6,7 @@ const { Container, Service, HTTPService, TransportService, DnsService, Node, Sit const { requireAuth } = require('../middlewares'); const ProxmoxApi = require('../utils/proxmox-api'); const serviceMap = require('../data/services.json'); -// Imported from utils as requested -const { isApiRequest } = require('../utils/http-utils'); +const { isApiRequest } = require('../utils/http'); /** * Normalize a Docker image reference to full format: host/org/image:tag From af40070fdd0048093e61366ce4c90e2daca3dec0 Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Fri, 6 Feb 2026 10:42:27 -0700 Subject: [PATCH 13/16] Patched security fixes regarding unauthorized API calls --- create-a-container/routers/containers.js | 65 +++++++++--------------- create-a-container/utils/http.js | 13 +++++ 2 files changed, 36 insertions(+), 42 deletions(-) create mode 100644 create-a-container/utils/http.js diff --git a/create-a-container/routers/containers.js b/create-a-container/routers/containers.js index ce6ef128..8aa3bbbf 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -6,6 +6,7 @@ const { Container, Service, HTTPService, TransportService, DnsService, Node, Sit const { requireAuth } = require('../middlewares'); const ProxmoxApi = require('../utils/proxmox-api'); const serviceMap = require('../data/services.json'); +const { isApiRequest } = require('../utils/http'); /** * Normalize a Docker image reference to full format: host/org/image:tag @@ -54,12 +55,6 @@ function normalizeDockerRef(ref) { return `${host}/${org}/${image}:${tag}`; } -// Helper to detect API bearer requests -function isApiRequest(req) { - const accept = (req.get('accept') || '').toLowerCase(); - return accept.includes('application/json') || accept.includes('application/vnd.api+json'); -} - // GET /sites/:siteId/containers/new - List available templates via API or HTML form router.get('/new', requireAuth, async (req, res) => { const siteId = parseInt(req.params.siteId, 10); @@ -131,48 +126,30 @@ router.get('/new', requireAuth, async (req, res) => { }); // GET /sites/:siteId/containers -router.get('/', async (req, res) => { - if (isApiRequest(req)) { - try { - const siteId = parseInt(req.params.siteId, 10); - const site = await Site.findByPk(siteId); - if (!site) return res.status(404).json([]); - - 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' }); - } - } - - await new Promise(resolve => requireAuth(req, res, resolve)); - if (res.headersSent) return; - +router.get('/', requireAuth, async (req, res) => { const siteId = parseInt(req.params.siteId, 10); const site = await Site.findByPk(siteId); if (!site) { - await req.flash('error', 'Site not found'); - return res.redirect('/sites'); + if (isApiRequest(req)) { + return res.status(404).json({ error: 'Site not found' }); + } else { + await req.flash('error', 'Site not found'); + return res.redirect('/sites'); + } } const nodes = await Node.findAll({ where: { siteId }, attributes: ['id'] }); const nodeIds = nodes.map(n => n.id); + const { hostname } = req.query; + const where = { + username: req.session.user, + nodeId: nodeIds + }; + if (hostname) where.hostname = hostname; + const containers = await Container.findAll({ - where: { - username: req.session.user, - nodeId: nodeIds - }, + where, include: [ { association: 'services', @@ -204,7 +181,11 @@ router.get('/', async (req, res) => { }; }); - return res.render('containers/index', { rows, site, req }); + if (isApiRequest(req)) { + return res.json({ containers: rows }); + } else { + return res.render('containers/index', { rows, site, req }); + } }); // GET /sites/:siteId/containers/:id/edit @@ -517,7 +498,7 @@ router.post('/', async (req, res) => { }); // PUT /sites/:siteId/containers/:id -router.put('/:id', async (req, res) => { +router.put('/:id', requireAuth, async (req, res) => { const siteId = parseInt(req.params.siteId, 10); const containerId = parseInt(req.params.id, 10); @@ -645,7 +626,7 @@ router.put('/:id', async (req, res) => { }); // DELETE /sites/:siteId/containers/:id -router.delete('/:id', async (req, res) => { +router.delete('/:id', requireAuth, async (req, res) => { const siteId = parseInt(req.params.siteId, 10); const containerId = parseInt(req.params.id, 10); diff --git a/create-a-container/utils/http.js b/create-a-container/utils/http.js new file mode 100644 index 00000000..50ec87d8 --- /dev/null +++ b/create-a-container/utils/http.js @@ -0,0 +1,13 @@ +/** + * Helper to detect API bearer requests + * @param {Object} req - Express request object + * @returns {boolean} - True if this is an API request + */ +function isApiRequest(req) { + const accept = (req.get('accept') || '').toLowerCase(); + return accept.includes('application/json') || accept.includes('application/vnd.api+json'); +} + +module.exports = { + isApiRequest +}; From 2497fe519992080539cdc3bfe25d9f04cfd0d596 Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Fri, 6 Feb 2026 10:51:43 -0700 Subject: [PATCH 14/16] authentication debugging --- create-a-container/middlewares/index.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/create-a-container/middlewares/index.js b/create-a-container/middlewares/index.js index 971bd169..828bd1d0 100644 --- a/create-a-container/middlewares/index.js +++ b/create-a-container/middlewares/index.js @@ -16,12 +16,14 @@ async function requireAuth(req, res, next) { const authHeader = req.get('Authorization'); if (authHeader && authHeader.startsWith('Bearer ')) { const apiKey = authHeader.substring(7); + console.log(`[AUTH DEBUG] Received API key, length: ${apiKey.length}`); if (apiKey) { const { ApiKey, User } = require('../models'); const { extractKeyPrefix } = require('../utils/apikey'); const keyPrefix = extractKeyPrefix(apiKey); + console.log(`[AUTH DEBUG] Extracted key prefix: ${keyPrefix}`); const apiKeys = await ApiKey.findAll({ where: { keyPrefix }, @@ -32,8 +34,25 @@ async function requireAuth(req, res, next) { }] }); + console.log(`[AUTH DEBUG] Found ${apiKeys.length} API keys with matching prefix`); + + if (apiKeys.length === 0) { + console.log(`[AUTH DEBUG] No API keys found in database with prefix: ${keyPrefix}`); + console.log(`[AUTH DEBUG] Listing all API key prefixes in database...`); + const allKeys = await ApiKey.findAll({ attributes: ['keyPrefix', 'description', 'uidNumber'] }); + console.log(`[AUTH DEBUG] All API keys:`, allKeys.map(k => ({ prefix: k.keyPrefix, desc: k.description, uid: k.uidNumber }))); + } + for (const storedKey of apiKeys) { + console.log(`[AUTH DEBUG] Validating key for user: ${storedKey.user?.uid || 'NO USER'}`); + if (!storedKey.user) { + console.log(`[AUTH DEBUG] API key has no associated user! uidNumber: ${storedKey.uidNumber}`); + continue; + } + const isValid = await storedKey.validateKey(apiKey); + console.log(`[AUTH DEBUG] Key validation result: ${isValid}`); + if (isValid) { req.user = storedKey.user; req.apiKey = storedKey; @@ -50,10 +69,14 @@ async function requireAuth(req, res, next) { console.error('Failed to update API key last used timestamp:', err); }); + console.log(`[AUTH DEBUG] ✓ Authentication successful for user: ${storedKey.user.uid}`); return next(); } } + console.log(`[AUTH DEBUG] ✗ No valid API key matched`); } + } else { + console.log(`[AUTH DEBUG] No Bearer token in Authorization header. Header value: ${authHeader ? authHeader.substring(0, 20) + '...' : 'none'}`); } // Neither session nor API key authentication succeeded From 3436dd26c80695c6a3095b32a002354e6404829e Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Fri, 6 Feb 2026 10:56:08 -0700 Subject: [PATCH 15/16] Update index.js --- create-a-container/middlewares/index.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/create-a-container/middlewares/index.js b/create-a-container/middlewares/index.js index 828bd1d0..429fdb41 100644 --- a/create-a-container/middlewares/index.js +++ b/create-a-container/middlewares/index.js @@ -14,16 +14,18 @@ async function requireAuth(req, res, next) { // Try API key authentication const authHeader = req.get('Authorization'); + console.log(`[AUTH DEBUG] Authorization header: "${authHeader || 'NONE'}" (length: ${authHeader?.length || 0})`); + if (authHeader && authHeader.startsWith('Bearer ')) { const apiKey = authHeader.substring(7); - console.log(`[AUTH DEBUG] Received API key, length: ${apiKey.length}`); + console.log(`[AUTH DEBUG] Extracted API key, length: ${apiKey.length}, first 8 chars: ${apiKey.substring(0, 8)}`); - if (apiKey) { + if (apiKey && apiKey.length > 0) { const { ApiKey, User } = require('../models'); const { extractKeyPrefix } = require('../utils/apikey'); const keyPrefix = extractKeyPrefix(apiKey); - console.log(`[AUTH DEBUG] Extracted key prefix: ${keyPrefix}`); + console.log(`[AUTH DEBUG] Key prefix: ${keyPrefix}`); const apiKeys = await ApiKey.findAll({ where: { keyPrefix }, @@ -74,9 +76,11 @@ async function requireAuth(req, res, next) { } } console.log(`[AUTH DEBUG] ✗ No valid API key matched`); + } else { + console.log(`[AUTH DEBUG] API key is empty after extraction!`); } } else { - console.log(`[AUTH DEBUG] No Bearer token in Authorization header. Header value: ${authHeader ? authHeader.substring(0, 20) + '...' : 'none'}`); + console.log(`[AUTH DEBUG] Authorization header does not start with "Bearer " (case-sensitive, note the space)`); } // Neither session nor API key authentication succeeded From 4d872cee558b90010e42ce01896dbc73ff7171e5 Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Fri, 6 Feb 2026 11:02:33 -0700 Subject: [PATCH 16/16] Remove debugging for troubleshooting --- create-a-container/middlewares/index.js | 29 +------------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/create-a-container/middlewares/index.js b/create-a-container/middlewares/index.js index 429fdb41..971bd169 100644 --- a/create-a-container/middlewares/index.js +++ b/create-a-container/middlewares/index.js @@ -14,18 +14,14 @@ async function requireAuth(req, res, next) { // Try API key authentication const authHeader = req.get('Authorization'); - console.log(`[AUTH DEBUG] Authorization header: "${authHeader || 'NONE'}" (length: ${authHeader?.length || 0})`); - if (authHeader && authHeader.startsWith('Bearer ')) { const apiKey = authHeader.substring(7); - console.log(`[AUTH DEBUG] Extracted API key, length: ${apiKey.length}, first 8 chars: ${apiKey.substring(0, 8)}`); - if (apiKey && apiKey.length > 0) { + if (apiKey) { const { ApiKey, User } = require('../models'); const { extractKeyPrefix } = require('../utils/apikey'); const keyPrefix = extractKeyPrefix(apiKey); - console.log(`[AUTH DEBUG] Key prefix: ${keyPrefix}`); const apiKeys = await ApiKey.findAll({ where: { keyPrefix }, @@ -36,25 +32,8 @@ async function requireAuth(req, res, next) { }] }); - console.log(`[AUTH DEBUG] Found ${apiKeys.length} API keys with matching prefix`); - - if (apiKeys.length === 0) { - console.log(`[AUTH DEBUG] No API keys found in database with prefix: ${keyPrefix}`); - console.log(`[AUTH DEBUG] Listing all API key prefixes in database...`); - const allKeys = await ApiKey.findAll({ attributes: ['keyPrefix', 'description', 'uidNumber'] }); - console.log(`[AUTH DEBUG] All API keys:`, allKeys.map(k => ({ prefix: k.keyPrefix, desc: k.description, uid: k.uidNumber }))); - } - for (const storedKey of apiKeys) { - console.log(`[AUTH DEBUG] Validating key for user: ${storedKey.user?.uid || 'NO USER'}`); - if (!storedKey.user) { - console.log(`[AUTH DEBUG] API key has no associated user! uidNumber: ${storedKey.uidNumber}`); - continue; - } - const isValid = await storedKey.validateKey(apiKey); - console.log(`[AUTH DEBUG] Key validation result: ${isValid}`); - if (isValid) { req.user = storedKey.user; req.apiKey = storedKey; @@ -71,16 +50,10 @@ async function requireAuth(req, res, next) { console.error('Failed to update API key last used timestamp:', err); }); - console.log(`[AUTH DEBUG] ✓ Authentication successful for user: ${storedKey.user.uid}`); return next(); } } - console.log(`[AUTH DEBUG] ✗ No valid API key matched`); - } else { - console.log(`[AUTH DEBUG] API key is empty after extraction!`); } - } else { - console.log(`[AUTH DEBUG] Authorization header does not start with "Bearer " (case-sensitive, note the space)`); } // Neither session nor API key authentication succeeded