-
Notifications
You must be signed in to change notification settings - Fork 4
Admin API and container deletion #104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
runleveldev
merged 20 commits into
main
from
88-uniqueness-testing-for-http-services-externalhostname
Nov 10, 2025
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
860c0b2
fix #88: add uniqueness constraints for services
runleveldev c81803f
fix #99: disable sequelize logging in production
runleveldev 88ab113
add node to containers model
runleveldev c3bf3e2
fix #85: add node determination to server.js
runleveldev 607784b
Add Nodes model
runleveldev be01358
fix #81: add delete /containers/:id route
runleveldev 9d93641
fix: improve error handling in container deletion route
runleveldev ba44e69
feat: add flash messages for success and error notifications in conta…
runleveldev 1200eec
feat: implement authentication middleware and add nodes management ro…
runleveldev 20734cc
feat: add tokenId and secret fields to Nodes model and update related…
runleveldev c7ef020
feat: add admin access control and update views for admin users
runleveldev 04e2e37
fix #105: add sessions table migration and configure Sequelize sessio…
runleveldev 6b19422
Potential fix for code scanning alert no. 31: Server-side request for…
runleveldev d7c746e
refactor: simplify admin access check in requireAdmin middleware
runleveldev 3f267fa
fix: update tlsVerify handling in node creation and update routes
runleveldev 58f5e48
fix: update container retrieval to use full username from session
runleveldev 9772336
fix: enhance accessibility by adding aria-labels to buttons in contai…
runleveldev 8dba8d0
fix: remove error handling for unsuccessful container deletion response
runleveldev 843c2bd
refactor: streamline API request detection in authentication middleware
runleveldev f123f63
Merge branch 'main' into 88-uniqueness-testing-for-http-services-exte…
runleveldev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| function isApiRequest(req) { | ||
| const acceptsJSON = req.get('Accept') && req.get('Accept').includes('application/json'); | ||
| const isAjax = req.get('X-Requested-With') === 'XMLHttpRequest'; | ||
| const isApiPath = req.path && req.path.startsWith('/api/'); | ||
| return acceptsJSON || isAjax || isApiPath; | ||
| } | ||
|
|
||
| // Authentication middleware (single) --- | ||
| // Detect API requests and browser requests. API requests return 401 JSON, browser requests redirect to /login. | ||
| function requireAuth(req, res, next) { | ||
| if (req.session && req.session.user) return next(); | ||
| if (isApiRequest(req)) | ||
| return res.status(401).json({ error: 'Unauthorized' }); | ||
|
|
||
| // Otherwise treat as a browser route: include the original URL as a redirect parameter | ||
| const original = req.originalUrl || req.url || '/'; | ||
| const redirectTo = '/login?redirect=' + encodeURIComponent(original); | ||
| return res.redirect(redirectTo); | ||
| } | ||
|
|
||
| function requireAdmin(req, res, next) { | ||
| if (req.session && req.session.isAdmin) return next(); | ||
| if (isApiRequest(req)) | ||
| return res.status(403).json({ error: 'Forbidden: Admin access required' }); | ||
| return res.status(403).send('Forbidden: Admin access required'); | ||
| } | ||
|
|
||
| module.exports = { requireAuth, requireAdmin }; |
24 changes: 24 additions & 0 deletions
24
create-a-container/migrations/20251104160710-add-service-uniqueness-constraints.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| 'use strict'; | ||
|
|
||
| /** @type {import('sequelize-cli').Migration} */ | ||
| module.exports = { | ||
| async up (queryInterface, Sequelize) { | ||
| // For HTTP services: externalHostname must be unique (NULL values are ignored by unique indexes) | ||
| await queryInterface.addIndex('Services', ['externalHostname'], { | ||
| name: 'services_http_unique_hostname', | ||
| unique: true | ||
| }); | ||
|
|
||
| // For TCP/UDP services: (type, externalPort) must be unique | ||
| await queryInterface.addIndex('Services', ['type', 'externalPort'], { | ||
| name: 'services_layer4_unique_port', | ||
| unique: true | ||
| }); | ||
| }, | ||
|
|
||
| async down (queryInterface, Sequelize) { | ||
| // Remove unique constraints | ||
| await queryInterface.removeIndex('Services', 'services_http_unique_hostname'); | ||
| await queryInterface.removeIndex('Services', 'services_layer4_unique_port'); | ||
| } | ||
| }; |
62 changes: 62 additions & 0 deletions
62
create-a-container/migrations/20251104184238-add-node-field-to-containers.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| 'use strict'; | ||
|
|
||
| /** @type {import('sequelize-cli').Migration} */ | ||
| module.exports = { | ||
| async up (queryInterface, Sequelize) { | ||
| // Step 1: Add the node column (allow NULL temporarily) | ||
| await queryInterface.addColumn('Containers', 'node', { | ||
| type: Sequelize.STRING(255), | ||
| allowNull: true | ||
| }); | ||
|
|
||
| // Step 2: Populate node based on aiContainer values | ||
| // FORTWAYNE -> mie-phxdc-ai-pve1 | ||
| await queryInterface.sequelize.query( | ||
| "UPDATE Containers SET node = 'mie-phxdc-ai-pve1' WHERE aiContainer = 'FORTWAYNE'" | ||
| ); | ||
|
|
||
| // PHOENIX -> intern-phxdc-pve3-ai | ||
| await queryInterface.sequelize.query( | ||
| "UPDATE Containers SET node = 'intern-phxdc-pve3-ai' WHERE aiContainer = 'PHOENIX'" | ||
| ); | ||
|
|
||
| // N + odd containerId -> intern-phxdc-pve1 | ||
| await queryInterface.sequelize.query( | ||
| "UPDATE Containers SET node = 'intern-phxdc-pve1' WHERE aiContainer = 'N' AND MOD(containerId, 2) = 1" | ||
| ); | ||
|
|
||
| // N + even containerId -> intern-phxdc-pve2 | ||
| await queryInterface.sequelize.query( | ||
| "UPDATE Containers SET node = 'intern-phxdc-pve2' WHERE aiContainer = 'N' AND MOD(containerId, 2) = 0" | ||
| ); | ||
|
|
||
| // Step 3: Make node NOT NULL | ||
| await queryInterface.changeColumn('Containers', 'node', { | ||
| type: Sequelize.STRING(255), | ||
| allowNull: false | ||
| }); | ||
|
|
||
| // Step 4: Remove unique constraint from containerId | ||
| await queryInterface.removeIndex('Containers', 'containerId'); | ||
|
|
||
| // Step 5: Add unique constraint on (node, containerId) | ||
| await queryInterface.addIndex('Containers', ['node', 'containerId'], { | ||
| name: 'containers_node_container_id_unique', | ||
| unique: true | ||
| }); | ||
| }, | ||
|
|
||
| async down (queryInterface, Sequelize) { | ||
| // Remove the unique constraint on (node, containerId) | ||
| await queryInterface.removeIndex('Containers', 'containers_node_container_id_unique'); | ||
|
|
||
| // Restore unique constraint on containerId | ||
| await queryInterface.addIndex('Containers', ['containerId'], { | ||
| name: 'containerId', | ||
| unique: true | ||
| }); | ||
|
|
||
| // Remove the node column | ||
| await queryInterface.removeColumn('Containers', 'node'); | ||
| } | ||
| }; |
46 changes: 46 additions & 0 deletions
46
create-a-container/migrations/20251104193601-create-node.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| 'use strict'; | ||
| /** @type {import('sequelize-cli').Migration} */ | ||
| module.exports = { | ||
| async up(queryInterface, Sequelize) { | ||
| await queryInterface.createTable('Nodes', { | ||
| id: { | ||
| allowNull: false, | ||
| autoIncrement: true, | ||
| primaryKey: true, | ||
| type: Sequelize.INTEGER | ||
| }, | ||
| name: { | ||
| type: Sequelize.STRING(255), | ||
| allowNull: false, | ||
| unique: true | ||
| }, | ||
| apiUrl: { | ||
| type: Sequelize.STRING(255), | ||
| allowNull: true | ||
| }, | ||
| tlsVerify: { | ||
| type: Sequelize.BOOLEAN, | ||
| allowNull: true | ||
| }, | ||
| createdAt: { | ||
| allowNull: false, | ||
| type: Sequelize.DATE | ||
| }, | ||
| updatedAt: { | ||
| allowNull: false, | ||
| type: Sequelize.DATE | ||
| } | ||
| }); | ||
|
|
||
| // Seed the Nodes table with existing node values from Containers | ||
| await queryInterface.sequelize.query(` | ||
| INSERT INTO Nodes (name, apiUrl, tlsVerify, createdAt, updatedAt) | ||
| SELECT DISTINCT node, NULL, NULL, NOW(), NOW() | ||
| FROM Containers | ||
| ORDER BY node | ||
| `); | ||
| }, | ||
| async down(queryInterface, Sequelize) { | ||
| await queryInterface.dropTable('Nodes'); | ||
| } | ||
| }; |
78 changes: 78 additions & 0 deletions
78
create-a-container/migrations/20251104193722-convert-container-node-to-node-id.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| 'use strict'; | ||
|
|
||
| /** @type {import('sequelize-cli').Migration} */ | ||
| module.exports = { | ||
| async up (queryInterface, Sequelize) { | ||
| // Step 1: Add nodeId column (temporarily nullable) | ||
| await queryInterface.addColumn('Containers', 'nodeId', { | ||
| type: Sequelize.INTEGER, | ||
| allowNull: true, | ||
| references: { | ||
| model: 'Nodes', | ||
| key: 'id' | ||
| }, | ||
| onUpdate: 'CASCADE', | ||
| onDelete: 'RESTRICT' | ||
| }); | ||
|
|
||
| // Step 2: Populate nodeId based on node string values | ||
| await queryInterface.sequelize.query( | ||
| "UPDATE Containers c JOIN Nodes n ON c.node = n.name SET c.nodeId = n.id" | ||
| ); | ||
|
|
||
| // Step 3: Make nodeId NOT NULL | ||
| await queryInterface.changeColumn('Containers', 'nodeId', { | ||
| type: Sequelize.INTEGER, | ||
| allowNull: false, | ||
| references: { | ||
| model: 'Nodes', | ||
| key: 'id' | ||
| }, | ||
| onUpdate: 'CASCADE', | ||
| onDelete: 'RESTRICT' | ||
| }); | ||
|
|
||
| // Step 4: Remove old unique constraint on (node, containerId) | ||
| await queryInterface.removeIndex('Containers', 'containers_node_container_id_unique'); | ||
|
|
||
| // Step 5: Add new unique constraint on (nodeId, containerId) | ||
| await queryInterface.addIndex('Containers', ['nodeId', 'containerId'], { | ||
| name: 'containers_node_id_container_id_unique', | ||
| unique: true | ||
| }); | ||
|
|
||
| // Step 6: Remove the old node column | ||
| await queryInterface.removeColumn('Containers', 'node'); | ||
| }, | ||
|
|
||
| async down (queryInterface, Sequelize) { | ||
| // Add back the node column | ||
| await queryInterface.addColumn('Containers', 'node', { | ||
| type: Sequelize.STRING(255), | ||
| allowNull: true | ||
| }); | ||
|
|
||
| // Populate node from nodeId | ||
| await queryInterface.sequelize.query( | ||
| "UPDATE Containers c JOIN Nodes n ON c.nodeId = n.id SET c.node = n.name" | ||
| ); | ||
|
|
||
| // Make node NOT NULL | ||
| await queryInterface.changeColumn('Containers', 'node', { | ||
| type: Sequelize.STRING(255), | ||
| allowNull: false | ||
| }); | ||
|
|
||
| // Remove new unique constraint | ||
| await queryInterface.removeIndex('Containers', 'containers_node_id_container_id_unique'); | ||
|
|
||
| // Restore old unique constraint | ||
| await queryInterface.addIndex('Containers', ['node', 'containerId'], { | ||
| name: 'containers_node_container_id_unique', | ||
| unique: true | ||
| }); | ||
|
|
||
| // Remove nodeId column | ||
| await queryInterface.removeColumn('Containers', 'nodeId'); | ||
| } | ||
| }; |
23 changes: 23 additions & 0 deletions
23
create-a-container/migrations/20251105145958-add-api-token-to-nodes.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| 'use strict'; | ||
|
|
||
| /** @type {import('sequelize-cli').Migration} */ | ||
| module.exports = { | ||
| async up (queryInterface, Sequelize) { | ||
| await queryInterface.addColumn('Nodes', 'tokenId', { | ||
| type: Sequelize.STRING(255), | ||
| allowNull: true, | ||
| after: 'apiUrl' | ||
| }); | ||
|
|
||
| await queryInterface.addColumn('Nodes', 'secret', { | ||
| type: Sequelize.STRING(255), | ||
| allowNull: true, | ||
| after: 'tokenId' | ||
| }); | ||
| }, | ||
|
|
||
| async down (queryInterface, Sequelize) { | ||
| await queryInterface.removeColumn('Nodes', 'tokenId'); | ||
| await queryInterface.removeColumn('Nodes', 'secret'); | ||
| } | ||
| }; |
34 changes: 34 additions & 0 deletions
34
create-a-container/migrations/20251105155228-create-sessions-table.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| 'use strict'; | ||
|
|
||
| /** @type {import('sequelize-cli').Migration} */ | ||
| module.exports = { | ||
| async up (queryInterface, Sequelize) { | ||
| await queryInterface.createTable('Sessions', { | ||
| session_id: { | ||
| type: Sequelize.STRING(32), | ||
| primaryKey: true, | ||
| allowNull: false | ||
| }, | ||
| expires: { | ||
| type: Sequelize.DATE, | ||
| allowNull: true | ||
| }, | ||
| data: { | ||
| type: Sequelize.TEXT, | ||
| allowNull: true | ||
| }, | ||
| createdAt: { | ||
| allowNull: false, | ||
| type: Sequelize.DATE | ||
| }, | ||
| updatedAt: { | ||
| allowNull: false, | ||
| type: Sequelize.DATE | ||
| } | ||
| }); | ||
| }, | ||
|
|
||
| async down (queryInterface, Sequelize) { | ||
| await queryInterface.dropTable('Sessions'); | ||
| } | ||
| }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.