Browse Source

Improvements to permissions system

lllllllillllllillll 11 months ago
parent
commit
b4f2b1f64f

+ 5 - 6
README.md

@@ -1,5 +1,5 @@
 <h3 align="center"><img width="150" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/public/img/logo.png"></h3>
-<h4 align="center">DweebUI Beta v0.70 ( :fire: Experimental :fire: )</h4>
+<h4 align="center">DweebUI v0.70 ( :fire: Experimental :fire: )</h4>
 <h3 align="center">Free and Open-Source WebUI For Managing Your Containers.</h3>
 <p align="center">
     <a href=""><img src="https://img.shields.io/github/stars/lllllllillllllillll/DweebUI?style=flat"/></a>
@@ -16,7 +16,7 @@
 
 * [x] A dynamically updating dashboard that displays server metrics along with container metrics and container controls.
 * [x] Multi-user support with permissions system.
-* [ ] Display and control docker containers from multiple remote hosts (planned).
+* [ ] Display and control docker containers from multiple remote hosts (in development).
 * [x] Container actions: Start, Stop, Pause, Restart, View Details, View Logs.
 * [x] Windows, Linux, and MacOS compatable.
 * [x] Light/Dark Mode.
@@ -53,7 +53,7 @@ services:
     ports:
       - 8000:8000
     volumes:
-      - dweebui:/app/config
+      - dweebui:/app
       # Docker socket
       - /var/run/docker.sock:/var/run/docker.sock
       # Podman socket
@@ -69,15 +69,14 @@ networks:
   dweebui_net:
     driver: bridge
 ```
-
-[Windows and MacOS Setup](https://github.com/lllllllillllllillll/DweebUI/wiki/Setup)
-
 Compose setup:
 
 * Paste the above content into a file named ```docker-compose.yml``` then place it in a folder named ```dweebui```.
 * Open a terminal in the ```dweebui``` folder, then enter ```docker compose up -d```.
 * You may need to use ```docker-compose up -d``` or execute the command as root with either ```sudo docker compose up -d``` or ```sudo docker-compose up -d```.
 
+[Windows and MacOS Setup](https://github.com/lllllllillllllillll/DweebUI/wiki/Setup)
+[Troubleshooting](https://github.com/lllllllillllllillll/DweebUI/wiki/Troubleshooting)
 
 
 ## Credits

+ 1 - 1
compose.yaml

@@ -11,7 +11,7 @@ services:
     ports:
       - 8000:8000
     volumes:
-      - dweebui:/app/config
+      - dweebui:/app
       # Docker socket
       - /var/run/docker.sock:/var/run/docker.sock
       # Podman socket

+ 4 - 4
controllers/account.js

@@ -8,7 +8,7 @@ export const Account = async (req, res) => {
         res.render("account", {
             first_name: 'Localhost',
             last_name: 'Localhost',
-            name: 'Localhost',
+            username: 'Localhost',
             id: 0,
             email: 'admin@localhost',
             role: 'admin',
@@ -28,16 +28,16 @@ export const Account = async (req, res) => {
         return;
     }
     
-    let user = await User.findOne({ where: { UUID: req.session.UUID }});
+    let user = await User.findOne({ where: { userID: req.session.userID }});
 
     res.render("account", {
         first_name: user.name,
         last_name: user.name,
-        name: user.name,
+        username: req.session.username,
         id: user.id,
         email: user.email,
         role: user.role,
-        avatar: req.session.user.charAt(0).toUpperCase(),
+        avatar: req.session.username.charAt(0).toUpperCase(),
         alert: '',
         link1: '',
         link2: '',

+ 4 - 4
controllers/apps.js

@@ -122,9 +122,9 @@ export const Apps = async (req, res) => {
 
 
   res.render("apps", {
-    name: req.session.user,
+    username: req.session.username,
     role: req.session.role,
-    avatar: req.session.user.charAt(0).toUpperCase(),
+    avatar: req.session.username.charAt(0).toUpperCase(),
     list_start: list_start + 1,
     list_end: list_end,
     app_count: app_count,
@@ -239,9 +239,9 @@ export const appSearch = async (req, res) => {
     apps_list += appCard;
   }
   res.render("apps", {
-      name: req.session.user,
+      username: req.session.username,
       role: req.session.role,
-      avatar: req.session.user.charAt(0).toUpperCase(),
+      avatar: req.session.username.charAt(0).toUpperCase(),
       list_start: list_start + 1,
       list_end: list_end,
       app_count: results.length,

+ 196 - 149
controllers/dashboard.js

@@ -1,77 +1,71 @@
 import { Readable } from 'stream';
-import { Permission, User, ServerSettings } from '../database/models.js';
-import { docker } from '../server.js';
 import { readFileSync } from 'fs';
 import { currentLoad, mem, networkStats, fsSize, dockerContainerStats } from 'systeminformation';
 import { Op } from 'sequelize';
+
 import Docker from 'dockerode';
 
+import { Permission, User, ServerSettings } from '../database/models.js';
+import { docker, docker2, docker3, docker4, host_list, host2_list, host3_list, host4_list } from '../server.js';
+
 let [ hidden, alert, newCards, stats ] = [ '', '', '', {} ];
 let logString = '';
 
-async function hostInfo(host) {
-    let info = await ServerSettings.findOne({ where: {key: host}});
-    try {
-        if (info.value != 'off' && info.value != '') {
-            let values = info.value.split(',');
-            return { tag: values[0], ip: values[1], port: values[2] };
-        }
-    } catch {
-        console.log(`${host}: No Value Set`);
-    }
-}
+// async function hostInfo(host) {
+//     let info = await ServerSettings.findOne({ where: {key: host}});
+//     try {
+//         if (info.value != 'off' && info.value != '') {
+//             let values = info.value.split(',');
+//             return { tag: values[0], ip: values[1], port: values[2] };
+//         }
+//     } catch {
+//         // console.log(`${host}: No Value Set`);
+//     }
+// }
 
 
-// The page
 export const Dashboard = async (req, res) => {
 
-    let name = req.session.user ;
-    let role = req.session.role;
-    alert = req.session.alert;
+    console.log(`Viewing Host: ${req.params.host}`);
 
-    let link1 = '';
-    let link2 = '';
-    let link3 = '';
-    let link4 = '';
+    let { link1, link2, link3, link4, link5, link6, link7, link8, link9 } = ['', '', '', '', '', '', '', '', ''];
 
-    let host2 = await hostInfo('host2');
-    if (host2) {
-        link2 = `<button class="btn text-yellow" name="host2" hx-post="/dashboard/checkhost" hx-trigger="load delay:2s " hx-swap="outerHTML">
-                    ${host2.tag}
-                </button>`;
+    // let host2 = await hostInfo('host2');
+    // let host3 = await hostInfo('host3');
+    // let host4 = await hostInfo('host4');
+
+    if (docker2 || docker3 || docker4) {
+        link1 = `<a href="/1/dashboard" class="btn text-green" name="host">
+                    Host 1
+                </a>`;
+        link5 = `<a href="/0/dashboard" class="btn text-green" name="hosts">
+                    All
+                </a>`;
     }
-    
-    let host3 = await hostInfo('host3');
-    if (host3) {
-        link3 = `<button class="btn text-yellow" name="host3" hx-post="/dashboard/checkhost" hx-trigger="load delay:2s " hx-swap="outerHTML">
-                    ${host3.tag}
-                </button>`;
+    if (docker2) { link2 = `<a href="/2/dashboard" class="btn text-green" name="host2">
+                    Host2
+                </a>`;
     }
-
-    let host4 = await hostInfo('host4');
-    if (host4) {
-        link4 = `<button class="btn text-yellow" name="host4" hx-post="/dashboard/checkhost" hx-trigger="load delay:2s " hx-swap="outerHTML">
-                    ${host4.tag}
-                </button>`;
+    if (docker3) { link3 = `<a href="/3/dashboard" class="btn text-green" name="host3">
+                    Host3
+                </a>`;
     }
-
-    if (host2 || host3 || host4) {
-        link1 = `<a href="#" class="btn text-green">
-                    Host 1
+    if (docker4) { link4 = `<a href="/4/dashboard" class="btn text-green" name="host4">
+                    Host4
                 </a>`;
     }
 
     
     res.render("dashboard", {
-        name: name,
-        avatar: name.charAt(0).toUpperCase(),
-        role: role,
-        alert: alert,
+        username: req.session.username,
+        avatar: req.session.username.charAt(0).toUpperCase(),
+        role: req.session.role,
+        alert: req.session.alert,
         link1: link1,
         link2: link2,
         link3: link3,
         link4: link4,
-        link5: '',
+        link5: link5,
         link6: '',
         link7: '',
         link8: '',
@@ -79,14 +73,73 @@ export const Dashboard = async (req, res) => {
     });
 }
 
-// The page actions
+
+export const ContainerAction = async (req, res) => {
+    // Assign values
+    let container_name = req.header('hx-trigger-name');
+    let container_id = req.header('hx-trigger');
+    let action = req.params.action;
+
+
+    if (container_id == 'reset') { 
+        console.log('Resetting view'); 
+        await Permission.update({ hide: false }, { where: { userID: req.session.userID } });
+        res.send('ok'); 
+        return;
+    }
+
+    // Inspect the container
+    let container = docker.getContainer(container_id);
+    let containerInfo = await container.inspect();
+    let state = containerInfo.State.Status;
+
+    console.log(`Container: ${container_name} ID: ${container_id} State: ${state} Action: ${action}`);
+
+    function status (state) {
+        return(`<span class="text-yellow align-items-center lh-1"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
+                        ${state}
+                </span>`);
+    }
+
+    if ((action == 'start') && (state == 'exited')) {
+        await container.start();
+        res.send(status('starting'));
+    } else if ((action == 'start') && (state == 'paused')) {
+        await container.unpause();
+        res.send(status('starting'));
+    } else if ((action == 'stop') && (state != 'exited')) {
+        await container.stop();
+        res.send(status('stopping'));
+    } else if ((action == 'pause') && (state == 'paused')) {
+        await container.unpause();
+        res.send(status('starting'));
+    }   else if ((action == 'pause') && (state == 'running')) {
+        await container.pause();
+        res.send(status('pausing'));
+    } else if (action == 'restart') {
+        await container.restart();
+        res.send(status('restarting'));
+    } else if (action == 'hide') {
+        let exists = await Permission.findOne({ where: { containerID: container_id, userID: req.session.userID }});
+        if (!exists) { const newPermission = await Permission.create({ containerName: container_name, containerID: container_id, username: req.session.username, userID: req.session.userID, hide: true }); }
+        else { exists.update({ hide: true }); }
+        // Array of hidden containers
+        hidden = await Permission.findAll({ where: { userID: req.session.userID, hide: true}}, { attributes: ['containerID'] });
+        // Map the container IDs
+        hidden = hidden.map((container) => container.containerID);
+        console.log(hidden);
+        res.send("ok");
+    }
+    
+}
+
 export const DashboardAction = async (req, res) => {
     let name = req.header('hx-trigger-name');
     let value = req.header('hx-trigger');
     let action = req.params.action;
     let modal = '';
 
-    console.log(`Action: ${action} Name: ${name} Value: ${value}`);
+    // console.log(`Action: ${action} Name: ${name} Value: ${value}`);
 
     if (req.body.search) { 
         console.log(req.body.search);
@@ -95,37 +148,51 @@ export const DashboardAction = async (req, res) => {
     }
     
     switch (action) {
-        case 'checkhost':
-            let link = '';
-            console.log(`checking host`);
-            let host_info = await hostInfo(name);
-            try {
-                var docker2 = new Docker({ protocol: 'http', host: host_info.ip, port: host_info.port });
-                let containers = await docker2.listContainers({ all: true });
-                console.log(containers);
-                link = `<button class="btn text-green" name="host2">
-                            ${host_info.tag}
-                        </button>`;
-            } catch {
-                console.log(`Error connecting to ${name}`);
-                link = `<button class="btn text-red" name="host2">
-                            ${host_info.tag}
-                        </button>`;
-            }
-            res.send(link);
-        return;
-        case 'permissions':
+        // case 'checkhost':
+        //     let link = '';
+        //     let host_info = await hostInfo(name);
+        //     try {
+        //         var docker2 = new Docker({ protocol: 'http', host: host_info.ip, port: host_info.port });
+        //         let containers = await docker2.listContainers({ all: true });
+        //         link = `<button class="btn text-green" name="host2">
+        //                     ${host_info.tag}
+        //                 </button>`;
+        //     } catch {
+        //         console.log(`Error connecting to ${name}`);
+        //         link = `<button class="btn text-red" name="host2">
+        //                     ${host_info.tag}
+        //                 </button>`;
+        //     }
+        //     res.send(link);
+        // return;
+        case 'permissions': // (Action = Selecting 'Permissions' from the dropdown) Creates the permissions modal 
+            // To capitalize the title
             let title = name.charAt(0).toUpperCase() + name.slice(1);
+            // Empty the permissions list
             let permissions_list = '';
+            // Get the container ID
+            let container = docker.getContainer(name);
+            let containerInfo = await container.inspect();
+            let container_id = containerInfo.Id;
+            // Get the body of the permissions modal
             let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
+            // Replace the title and container name in the modal
             permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
             permissions_modal = permissions_modal.replace(/PermissionsContainer/g, name);
-            let users = await User.findAll({ attributes: ['username', 'UUID']});
+            permissions_modal = permissions_modal.replace(/ContainerID/g, container_id);
+            // Get a list of all users
+            let users = await User.findAll({ attributes: ['username', 'userID']});
+            // Loop through each user to check what permissions they have
             for (let i = 0; i < users.length; i++) {
+                // Get the user_permissions form
                 let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
-                let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
-                if (!exists) { const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID}); }
-                let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
+                // Check if the user has any permissions for the container
+                let exists = await Permission.findOne({ where: { containerID: container_id, userID: users[i].userID }});
+                // Create an entry if one doesn't exist
+                if (!exists) { const newPermission = await Permission.create({ containerName: name, containerID: container_id, username: users[i].username, userID: users[i].userID }); }
+                // Get the permissions for the user
+                let permissions = await Permission.findOne({ where: { containerID: container_id, userID: users[i].userID }});
+                // Fill in the form values
                 if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
                 if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
                 if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
@@ -144,9 +211,14 @@ export const DashboardAction = async (req, res) => {
                 user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
                 user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
                 user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
+                user_permissions = user_permissions.replace(/PermissionsUserID/g, users[i].userID);
+                user_permissions = user_permissions.replace(/PermissionsID/g, container_id);
+                // Add the user entry to the permissions list
                 permissions_list += user_permissions;
             }
+            // Insert the user list into the permissions modal
             permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
+            // Send the permissions modal
             res.send(permissions_modal);
             return;
         case 'uninstall':
@@ -193,12 +265,15 @@ export const DashboardAction = async (req, res) => {
             newCards = '';
             return;
         case 'card':
+            // Check which cards the user has permissions for
             await userCards(req.session);
+            // Remove the container if it isn't in the user's list
             if (!req.session.container_list.find(c => c.container === name)) {
                 res.send('');
                 return;
             } else {
-                let details = await containerInfo(name);
+                // Get the container information and send the updated card
+                let details = await containerInfo(value);
                 let card = await createCard(details);
                 res.send(card);
                 return;
@@ -218,58 +293,19 @@ export const DashboardAction = async (req, res) => {
                 });
             });
             return;
-        case 'hide':
-            let user = req.session.user;
-            let exists = await Permission.findOne({ where: {containerName: name, user: user}});
-            if (!exists) { const newPermission = await Permission.create({ containerName: name, user: user, hide: true, userID: req.session.UUID}); }
-            else { exists.update({ hide: true }); }
-            hidden = await Permission.findAll({ where: {user: user, hide: true}}, { attributes: ['containerName'] });
-            hidden = hidden.map((container) => container.containerName);
-            res.send("ok");
-            return;
-        case 'reset':
-            await Permission.update({ hide: false }, { where: { user: req.session.user } });
-            res.send("ok");
-            return;
         case 'alert':
             req.session.alert = '';
             res.send('');
             return;
     }
-
-    function status (state) {
-        return(`<span class="text-yellow align-items-center lh-1"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
-                        ${state}
-                </span>`);
-    }
-
-    // Container actions
-    if ((action == 'start') && (value == 'stopped')) {
-        docker.getContainer(name).start();
-        res.send(status('starting'));
-    } else if ((action == 'start') && (value == 'paused')) {
-        docker.getContainer(name).unpause();
-        res.send(status('starting'));
-    } else if ((action == 'stop') && (value != 'stopped')) {
-        docker.getContainer(name).stop();
-        res.send(status('stopping'));
-    } else if ((action == 'pause') && (value == 'paused')) {
-        docker.getContainer(name).unpause();
-        res.send(status('starting'));
-    }   else if ((action == 'pause') && (value == 'running')) {
-        docker.getContainer(name).pause();
-        res.send(status('pausing'));
-    } else if (action == 'restart') {
-        docker.getContainer(name).restart();
-        res.send(status('restarting'));
-    } 
 }
 
-async function containerInfo (containerName) {
+async function containerInfo (containerID) {
     // get the container info
-    let container = docker.getContainer(containerName);
+    let container = docker.getContainer(containerID);
     let info = await container.inspect();
     let image = info.Config.Image;
+    let container_id = info.Id;
     // grab the service name from the end of the image name
     let service = image.split('/').pop();
     // remove the tag from the service name if it exists
@@ -295,9 +331,10 @@ async function containerInfo (containerName) {
     } catch {}
 
     let details = {
-        name: containerName,
+        name: info.Name.slice(1),
         image: image,
         service: service,
+        containerID: container_id,
         state: info.State.Status,
         external_port: external,
         internal_port: internal,
@@ -343,6 +380,7 @@ async function createCard (details) {
     // if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
 
     card = card.replace(/AppName/g, details.name);
+    card = card.replace(/AppID/g, details.containerID);
     card = card.replace(/AppShortName/g, shortname);
     card = card.replace(/AppIcon/g, app_icon);
     card = card.replace(/AppState/g, state);
@@ -356,45 +394,51 @@ async function createCard (details) {
     return card;
 }
 
+// Creates a list of containers that the user should be able to see.
 async function userCards (session) {
+    // Create an empty container list.
     session.container_list = [];
-    // check what containers the user wants hidden
-    let hidden = await Permission.findAll({ where: {user: session.user, hide: true}}, { attributes: ['containerName'] });
-    hidden = hidden.map((container) => container.containerName);
-    // check what containers the user has permission to view
-    let visable = await Permission.findAll({ where: { user: session.user, [Op.or]: [{ uninstall: true }, { edit: true }, { upgrade: true }, { start: true }, { stop: true }, { pause: true }, { restart: true }, { logs: true }, { view: true }] } });
-    visable = visable.map((container) => container.containerName);
-    // get all containers
+    // Check what containers the user has hidden.
+    let hidden = await Permission.findAll({ where: { userID: session.userID, hide: true }, attributes: ['containerID'], raw: true });
+    // Check which containers the user has permissions for.
+    let visable = await Permission.findAll({ where: { userID: session.userID, [Op.or]: [{ uninstall: true }, { edit: true }, { upgrade: true }, { start: true }, { stop: true }, { pause: true }, { restart: true }, { logs: true }, { view: true }] }, attributes: ['containerID'], raw: true});
+    // Get a list of all the containers.
     let containers = await docker.listContainers({ all: true });
-    // loop through containers
+    // Loop through the list of containers.
     for (let i = 0; i < containers.length; i++) {
-        let container_name = containers[i].Names[0].replace('/', '');
-        // skip hidden containers
-        if (hidden.includes(container_name)) { continue; }
-        // admin can see all containers that they don't have hidden
-        if (session.role == 'admin') { session.container_list.push({ container: container_name, state: containers[i].State }); }
-        // user can see any containers that they have any permissions for
-        else if (visable.includes(container_name)){ session.container_list.push({ container: container_name, state: containers[i].State }); }
+        // Get the container ID.
+        let containerID = containers[i].Id;
+        // Skip the container if it's ID is in the hidden list.
+        if (hidden.includes(containerID)) { console.log('skipped hidden container'); continue; }
+        // If the user is admin and they don't have it hidden, add it to the list.
+        if (session.role == 'admin') { session.container_list.push({ container: containerID, state: containers[i].State }); }
+        // Add the container if it's ID is in the visable list.
+        else if (visable.includes(containerID)){ session.container_list.push({ container: containerID, state: containers[i].State }); }
     }
-    // create a sent list if it doesn't exist
+    // Create the lists if they don't exist.
     if (!session.sent_list) { session.sent_list = []; }
     if (!session.update_list) { session.update_list = []; }
     if (!session.new_cards) { session.new_cards = []; }
 }
 
 async function updateDashboard (session) {
+    // Get the list of containers and the list of containers that have been sent.
     let container_list = session.container_list;
     let sent_list = session.sent_list;
     session.new_cards = [];
     session.update_list = [];
-    // loop through the containers list
+    // Loop through the containers list
     container_list.forEach(info => {
+        // Get the containerID and state
         let { container, state } = info;
+        // Check if the container is in the sent list
         let sent = sent_list.find(c => c.container === container);
+        // If it's not in the sent list, add it to the new cards list.
         if (!sent) { session.new_cards.push(container);}
+        // If it is in the sent list, check if the state has changed.
         else if (sent.state !== state) { session.update_list.push(container); }
     });
-    // loop through the sent list to see if any containers have been removed
+    // Loop through the sent list to see if any containers have been removed
     sent_list.forEach(info => {
         let { container } = info;
         let exists = container_list.find(c => c.container === container);
@@ -404,9 +448,9 @@ async function updateDashboard (session) {
 
 // HTMX server-side events
 export const SSE = async (req, res) => {
-    // set the headers for server-sent events
+    // Set the headers
     res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
-    // check for container changes every 500ms
+    // Check for container changes every 500ms
     let eventCheck = setInterval(async () => {
         await userCards(req.session);
         // check if the cards displayed are the same as what's in the session
@@ -476,25 +520,28 @@ export async function addAlert (session, type, message) {
 }
 
 export const UpdatePermissions = async (req, res) => {
-    let { user, container, reset_permissions } = req.body;
+    let { userID, container, containerID, reset_permissions } = req.body;
     let id = req.header('hx-trigger');
+
+    console.log(`User: ${userID} Container: ${container} ContainerID: ${containerID} Reset: ${reset_permissions}`);
+
     if (reset_permissions) {
-        await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false }, { where: { containerName: container} });
+        await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false }, { where: { containerID: containerID} });
         return;
     }
-    await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false}, { where: { containerName: container, user: user } });
+    await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false}, { where: { containerID: containerID, userID: userID } });
     Object.keys(req.body).forEach(async function(key) {
         if (key != 'user' && key != 'container') {
             let permissions = req.body[key];
-            if (permissions.includes('uninstall')) { await Permission.update({ uninstall: true }, { where: {containerName: container, user: user}}); }  
-            if (permissions.includes('edit')) { await Permission.update({ edit: true }, { where: {containerName: container, user: user}}); }   
-            if (permissions.includes('upgrade')) { await Permission.update({ upgrade: true }, { where: {containerName: container, user: user}}); }   
-            if (permissions.includes('start')) { await Permission.update({ start: true }, { where: {containerName: container, user: user}}); }   
-            if (permissions.includes('stop')) { await Permission.update({ stop: true }, { where: {containerName: container, user: user}}); }   
-            if (permissions.includes('pause')) { await Permission.update({ pause: true }, { where: {containerName: container, user: user}}); }   
-            if (permissions.includes('restart')) { await Permission.update({ restart: true }, { where: {containerName: container, user: user}}); }   
-            if (permissions.includes('logs')) { await Permission.update({ logs: true }, { where: {containerName: container, user: user}}); }
-            if (permissions.includes('view')) { await Permission.update({ view: true }, { where: {containerName: container, user: user}}); }
+            if (permissions.includes('uninstall')) { await Permission.update({ uninstall: true }, { where: { containerID: containerID, userID: userID}}); }  
+            if (permissions.includes('edit')) { await Permission.update({ edit: true }, { where: { containerID: containerID, userID: userID}}); }   
+            if (permissions.includes('upgrade')) { await Permission.update({ upgrade: true }, { where: { containerID: containerID, userID: userID}}); }   
+            if (permissions.includes('start')) { await Permission.update({ start: true }, { where: { containerID: containerID, userID: userID}}); }   
+            if (permissions.includes('stop')) { await Permission.update({ stop: true }, { where: { containerID: containerID, userID: userID}}); }   
+            if (permissions.includes('pause')) { await Permission.update({ pause: true }, { where: { containerID: containerID, userID: userID}}); }   
+            if (permissions.includes('restart')) { await Permission.update({ restart: true }, { where: { containerID: containerID, userID: userID}}); }   
+            if (permissions.includes('logs')) { await Permission.update({ logs: true }, { where: { containerID: containerID, userID: userID}}); }
+            if (permissions.includes('view')) { await Permission.update({ view: true }, { where: { containerID: containerID, userID: userID}}); }
         }  
     });
     if (id == 'submit') {

+ 4 - 2
controllers/images.js

@@ -5,6 +5,8 @@ export const Images = async function(req, res) {
 
     let action = req.params.action;
 
+    console.log(req.params.host);
+
     if (action == "remove") {
         let images = req.body.select;
 
@@ -101,9 +103,9 @@ export const Images = async function(req, res) {
 
     
     res.render("images", {
-        name: req.session.user,
+        username: req.session.username,
         role: req.session.role,
-        avatar: req.session.user.charAt(0).toUpperCase(),
+        avatar: req.session.username.charAt(0).toUpperCase(),
         image_list: image_list,
         image_count: images.length,
         alert: '',

+ 72 - 58
controllers/login.js

@@ -1,82 +1,96 @@
-import { User, Syslog } from '../database/models.js';
 import bcrypt from 'bcrypt';
+import { User, Syslog } from '../database/models.js';
 
+// Environment variable to disable authentication.
 const no_auth = process.env.NO_AUTH || false;
 
 
 export const Login = function(req,res){
-    if (req.session.user) { res.redirect("/logout"); }
+    if (req.session.username) { res.redirect("/dashboard"); }
     else { res.render("login",{ "error":"", }); }
 }
 
+
+export const Logout = function(req,res){
+    req.session.destroy(() => {
+        res.redirect("/login");
+    });
+}
+
+
 export const submitLogin = async function(req,res){
 
+    // Grab values from the form.
+    let { email, password } = req.body;
+
+    // Convert the email to lowercase.
+    email = email.toLowerCase();
+
+    // Create an admin session if NO_AUTH is enabled and the user is on localhost.
     if (no_auth && req.hostname == 'localhost') { 
-        req.session.user = 'Localhost';
-        req.session.UUID = '';
+        req.session.username = 'Localhost';
+        req.session.userID = '';
         req.session.role = 'admin';
         res.redirect("/dashboard");
         return;
     }
 
-    let { email, password } = req.body;
-    email = email.toLowerCase();
+    // Check that all fields are filled out.
+    if (!email || !password) {
+        res.render("login",{
+            "error":"Please fill in all fields.",
+        });
+        return;
+    }
 
-    if (email && password) {
-        let existingUser = await User.findOne({ where: {email:email}});
-        if (existingUser) {
-
-            let match = await bcrypt.compare(password,existingUser.password);
-
-            if (match) {
-                let currentDate = new Date();
-                let newLogin = currentDate.toLocaleString();
-                await User.update({lastLogin: newLogin}, {where: {UUID:existingUser.UUID}});
-
-                req.session.user = existingUser.username;
-                req.session.UUID = existingUser.UUID;
-                req.session.role = existingUser.role;
-                req.session.avatar = existingUser.avatar;
-
-                const syslog = await Syslog.create({
-                    user: req.session.user,
-                    email: email,
-                    event: "Successful Login",
-                    message: "User logged in successfully",
-                    ip: req.socket.remoteAddress
-                });
-                
-                res.redirect("/dashboard");
-            } else {
-
-                const syslog = await Syslog.create({
-                    user: null,
-                    email: email,
-                    event: "Bad Login",
-                    message: "Invalid password",
-                    ip: req.socket.remoteAddress
-                });
-
-                res.render("login",{
-                    "error":"Invalid password",
-                });
-            }
-        } else {
-            res.render("login",{
-                "error":"User with that email does not exist.",
-            });
-        }
-    } else {
-        res.status(400);
+    // Check that the user exists.
+    let user = await User.findOne({ where: { email: email }});
+    if (!user) {
         res.render("login",{
-            "error":"Please fill in all the fields.",
+            "error":"Invalid credentials.",
         });
+        return;
     }
-}
 
+    // Check that the password is correct.
+    let password_check = await bcrypt.compare( password, user.password);
 
-export const Logout = function(req,res){
-    req.session.destroy(() => {
-        res.redirect("/login");
+    // If the password is incorrect, log the failed login attempt.
+    if (!password_check) {
+        res.render("login",{
+            "error":"Invalid credentials.",
+        });
+        const syslog = await Syslog.create({
+            user: null,
+            email: email,
+            event: "Bad Login",
+            message: "Invalid password",
+            ip: req.socket.remoteAddress
+        });
+        return;
+    }
+
+    // Successful login. Create the user session.
+    req.session.username = user.username;
+    req.session.userID = user.userID;
+    req.session.role = user.role;
+
+    // Update the last login time.
+    let date = new Date();
+    let new_login = date.toLocaleString();
+    await User.update({ lastLogin: new_login }, { where: { userID: user.userID}});
+
+    // Create a login entry.
+    const syslog = await Syslog.create({
+        user: req.session.username,
+        email: email,
+        event: "Successful Login",
+        message: "User logged in successfully",
+        ip: req.socket.remoteAddress
     });
-}
+    
+    // Redirect to the dashboard.
+    res.redirect("/dashboard");
+}
+
+

+ 5 - 2
controllers/networks.js

@@ -4,6 +4,9 @@ import { docker } from '../server.js';
 export const Networks = async function(req, res) {
     let container_networks = [];
     let network_name = '';
+
+    console.log(req.params.host);
+
     // List all containers
     let containers = await docker.listContainers({ all: true });
     // Loop through the containers to find out which networks are being used
@@ -48,9 +51,9 @@ export const Networks = async function(req, res) {
     network_list += `</tbody>`
 
     res.render("networks", {
-        name: req.session.user,
+        username: req.session.username,
         role: req.session.role,
-        avatar: req.session.user.charAt(0).toUpperCase(),
+        avatar: req.session.username.charAt(0).toUpperCase(),
         network_list: network_list,
         network_count: networks.length,
         alert: '',

+ 85 - 73
controllers/register.js

@@ -1,11 +1,12 @@
-import { User, Syslog, Permission, ServerSettings } from '../database/models.js';
 import bcrypt from 'bcrypt';
+import { User, Syslog, Permission, ServerSettings } from '../database/models.js';
+
 
 export const Register = async function (req,res) {
 
     // Redirect to dashboard if user is already logged in.
     if(req.session.user){ res.redirect("/dashboard"); return; } 
-    
+
     // Continue to registration page if no users have been created.
     let users = await User.count();
     if (users == 0) {
@@ -14,7 +15,7 @@ export const Register = async function (req,res) {
             "error": "Creating admin account. Leave passphrase blank.",
         });
     } else {
-    // Check if registration is enabled.
+        // Check if registration is enabled.
         let registration = await ServerSettings.findOne({ where: {key: 'registration'}});
         if (registration.value == 'off') {
             res.render("login",{
@@ -32,14 +33,17 @@ export const Register = async function (req,res) {
 export const submitRegister = async function (req,res) {
 
     // Grab values from the form.
-    let { name, username, password, confirmPassword, passphrase } = req.body;
-    let email = req.body.email.toLowerCase();
+    let { name, username, email, password1, password2, passphrase } = req.body;
 
-    // Get the passphrase from the database.
-    let confirm_passphrase = await ServerSettings.findOne({ where: {key: 'registration'}});
+    // Convert the email to lowercase.
+    email = email.toLowerCase();
+
+    // Get the registration passphrase.
+    let registration_passphrase = await ServerSettings.findOne({ where: { key: 'registration' }});
+    registration_passphrase = registration_passphrase.value;
 
     // Create a log entry if the form is submitted with an invalid passphrase.
-    if (passphrase != confirm_passphrase.value) {
+    if (passphrase != registration_passphrase) {
         const syslog = await Syslog.create({
             user: username,
             email: email,
@@ -47,77 +51,85 @@ export const submitRegister = async function (req,res) {
             message: "Invalid secret",
             ip: req.socket.remoteAddress
         });
+        res.render("register",{
+            "error":"Invalid passphrase",
+        });
+        return;
     }
 
-    // Check that all fields are filled out and that the passphrase is correct.
-    if ((name && email && password && confirmPassword && username) && (passphrase == confirm_passphrase.value) && (password == confirmPassword)) {
-
-        async function userRole () {
-            let userCount = await User.count();
-            if (userCount == 0) { 
-                // Disable registration.
-                await ServerSettings.update({ value: 'off' }, { where: { key: 'registration' }}); 
-                return "admin"; 
-            } else { 
-                return "user"; 
-            }
-        }
+    // Check that all fields are filled out correctly.
+    if ((!name || !username || !email || !password1 || !password2) || (password1 != password2)) {
+        res.render("register",{
+            "error":"Missing field or password mismatch.",
+        });
+        return;
+    }
 
-        // Check if the email address has already been used.
-        let existingUser = await User.findOne({ where: {email:email}});
-        if (!existingUser) {
-            try {
-                // Create the user.
-                const user = await User.create({ 
-                    name: name,
-                    username: username,
-                    email: email,
-                    password: bcrypt.hashSync(password,10),
-                    role: await userRole(),
-                    group: 'all',
-                    lastLogin: new Date().toLocaleString(),
-                });
-
-                // make sure the user was created and get the UUID.
-                let newUser = await User.findOne({ where: {email:email}});
-                let match = await bcrypt.compare(password,newUser.password);
-
-                if (match) {  
-                    // Create the user session.
-                    req.session.user = newUser.username;
-                    req.session.UUID = newUser.UUID;
-                    req.session.role = newUser.role;
-
-                    // Create an entry in the permissions table.
-                    await Permission.create({ user: newUser.username, userID: newUser.UUID });
-
-                    // Create a log entry.
-                    const syslog = await Syslog.create({
-                        user: req.session.user,
-                        email: email,
-                        event: "Successful Registration",
-                        message: "User registered successfully",
-                        ip: req.socket.remoteAddress
-                    });
-                    res.redirect("/dashboard");
-                }
-
-            } catch {
-                res.render("register",{
-                    "error":"Something went wrong when creating account.",
-                });
-            }
+    // Make sure the username and email are unique.
+    let existing_username = await User.findOne({ where: {username:username}});
+    let existing_email = await User.findOne({ where: {email:email}});
+    if (existing_username || existing_email) {
+        res.render("register",{
+            "error":"Username or email already exists.",
+        });
+        return;
+    }
 
-        } else {
-                // return an error.
-                res.render("register",{
-                    "error":"User with that email already exists.",
-                });
-            }
+    // Make the user an admin and disable registration if there are no other users.
+    async function userRole () {
+        let userCount = await User.count();
+        if (userCount == 0) { 
+            await ServerSettings.update({ value: 'off' }, { where: { key: 'registration' }}); 
+            return "admin"; 
+        } else { 
+            return "user"; 
+        }
+    }
+
+    // Create the user.
+    const user = await User.create({ 
+        name: name,
+        username: username,
+        email: email,
+        password: bcrypt.hashSync(password1,10),
+        role: await userRole(),
+        group: 'all',
+        lastLogin: new Date().toLocaleString(),
+    });
+
+    // make sure the user was created and get the UUID.
+    let newUser = await User.findOne({ where: { email: email }});
+    let match = await bcrypt.compare( password1, newUser.password);
+
+    if (match) {  
+        // Create the user session.
+        req.session.username = newUser.username;
+        req.session.userID = newUser.userID;
+        req.session.role = newUser.role;
+
+        // Create an entry in the permissions table.
+        await Permission.create({ username: req.session.username, userID: req.session.userID });
+
+        // Create a log entry.
+        const syslog = await Syslog.create({
+            user: req.session.username,
+            email: email,
+            event: "Successful Registration",
+            message: "User registered successfully",
+            ip: req.socket.remoteAddress
+        });
+        res.redirect("/dashboard");
     } else {
-        // Redirect to the signup page.
+        // Create a log entry.
+        const syslog = await Syslog.create({
+            user: req.session.username,
+            email: email,
+            event: "Failed Registration",
+            message: "User not created",
+            ip: req.socket.remoteAddress
+        });
         res.render("register",{
-            "error":"Please fill in all the fields.",
+            "error":"User not created",
         });
     }
 }

+ 2 - 2
controllers/settings.js

@@ -64,9 +64,9 @@ export const Settings = async (req, res) => {
 
 
     res.render("settings", {
-        name: req.session.user,
+        username: req.session.username,
         role: req.session.role,
-        avatar: req.session.user.charAt(0).toUpperCase(),
+        avatar: req.session.username.charAt(0).toUpperCase(),
         alert: '',
         settings: settings,
         link1: '',

+ 4 - 10
controllers/supporters.js

@@ -1,18 +1,12 @@
-import { User } from "../database/models.js";
-
 export const Supporters = async (req, res) => {
     
-    let user = await User.findOne({ where: { UUID: req.session.UUID }});
     
 
     res.render("supporters", {
-        first_name: user.name,
-        last_name: user.name,
-        name: user.name,
-        id: user.id,
-        email: user.email,
-        role: user.role,
-        avatar: req.session.user.charAt(0).toUpperCase(),
+        
+        username: req.session.username,
+        role: req.session.role,
+        avatar: req.session.username.charAt(0).toUpperCase(),
         alert: '',
         link1: '',
         link2: '',

+ 2 - 2
controllers/syslogs.js

@@ -27,9 +27,9 @@ export const Syslogs = async function(req, res) {
     }
     
     res.render("syslogs", {
-        name: req.session.user || 'Dev',
+        username: req.session.username || 'Dev',
         role: req.session.role || 'Dev',
-        avatar: req.session.user.charAt(0).toUpperCase(),
+        avatar: req.session.username.charAt(0).toUpperCase(),
         logs: logs,
         alert: '',
         link1: '',

+ 2 - 2
controllers/users.js

@@ -52,9 +52,9 @@ export const Users = async (req, res) => {
 
 
     res.render("users", {
-        name: req.session.user,
+        username: req.session.username,
         role: req.session.role,
-        avatar: req.session.user.charAt(0).toUpperCase(),
+        avatar: req.session.username.charAt(0).toUpperCase(),
         user_list: user_list,
         alert: '',
         link1: '',

+ 5 - 10
controllers/volumes.js

@@ -4,6 +4,8 @@ export const Volumes = async function(req, res) {
     let container_volumes = [];
     let volume_list = '';
 
+    console.log(req.params.host);
+
     // Table header
     volume_list = `<thead>
                         <tr>
@@ -67,9 +69,9 @@ export const Volumes = async function(req, res) {
 
     
     res.render("volumes", {
-        name: req.session.user,
+        username: req.session.username,
         role: req.session.role,
-        avatar: req.session.user.charAt(0).toUpperCase(),
+        avatar: req.session.username.charAt(0).toUpperCase(),
         volume_list: volume_list,
         volume_count: volumes.length,
         alert: '',
@@ -118,11 +120,4 @@ export const removeVolume = async function(req, res) {
     }
 
     res.redirect("/volumes");
-}
-
-
-// docker.df(volume.Name).then((data) => {
-//     for (let key in data) {
-//         console.log(data[key]);
-//     }
-// });
+}

+ 6 - 6
database/models.js

@@ -16,6 +16,10 @@ export const User = sequelize.define('User', {
   name: {
     type: DataTypes.STRING
   },
+  userID: {
+    type: DataTypes.UUID,
+    defaultValue: DataTypes.UUIDV4,
+  },
   username: {
     type: DataTypes.STRING,
     allowNull: false
@@ -39,10 +43,6 @@ export const User = sequelize.define('User', {
   },
   lastLogin: {
     type: DataTypes.STRING
-  },
-  UUID: {
-    type: DataTypes.UUID,
-    defaultValue: DataTypes.UUIDV4,
   }
 });
 
@@ -114,7 +114,7 @@ export const Permission = sequelize.define('Permission', {
   containerID: {
     type: DataTypes.STRING,
   },
-  user: {
+  username: {
     type: DataTypes.STRING,
     allowNull: false
   },
@@ -248,7 +248,7 @@ export const UserSettings = sequelize.define('UserSettings', {
     autoIncrement: true,
     primaryKey: true
   },
-  uuid: {
+  userID: {
     type: DataTypes.STRING,
     allowNull: false
   },

+ 15 - 5
router/index.js

@@ -1,13 +1,10 @@
 import express from "express";
 export const router = express.Router();
 
-// Permissions middleware
-import { adminOnly, sessionCheck, permissionCheck } from "../utils/permissions.js";
-
 // Controllers
 import { Login, submitLogin, Logout } from "../controllers/login.js";
 import { Register, submitRegister } from "../controllers/register.js";
-import { Dashboard, DashboardAction, Stats, Chart, SSE, UpdatePermissions } from "../controllers/dashboard.js";
+import { Dashboard, DashboardAction, Stats, Chart, SSE, UpdatePermissions, ContainerAction } from "../controllers/dashboard.js";
 import { Apps, appSearch, InstallModal, ImportModal, LearnMore, Upload, removeTemplate } from "../controllers/apps.js";
 import { Users } from "../controllers/users.js";
 import { Images } from "../controllers/images.js";
@@ -20,25 +17,38 @@ import { Syslogs } from "../controllers/syslogs.js";
 import { Install } from "../utils/install.js"
 import { Uninstall } from "../utils/uninstall.js"
 
+// Permissions middleware
+import { adminOnly, sessionCheck, permissionCheck } from "../utils/permissions.js";
+
 // Routes
 router.get("/login", Login);
 router.post("/login", submitLogin);
 router.get("/logout", Logout);
+
 router.get("/register", Register);
 router.post("/register", submitRegister);  
 
 router.get("/", sessionCheck, Dashboard);
 router.get("/dashboard", sessionCheck, Dashboard);
-router.post("/dashboard/:action", sessionCheck, permissionCheck, DashboardAction);
+
+router.get("/:host?/dashboard", adminOnly, Dashboard);
+router.post("/:host?/dashboard/:action", sessionCheck, permissionCheck, DashboardAction);
+
+router.post("/:host?/container/:action", sessionCheck, permissionCheck, ContainerAction);
+
+
 router.get("/sse", sessionCheck, SSE);
 router.post("/updatePermissions", adminOnly, UpdatePermissions);
 router.get("/stats", sessionCheck, Stats);
 router.get("/chart", sessionCheck, Chart);
 
 router.get("/images", adminOnly, Images);
+router.get("/:host?/images", adminOnly, Images);
 router.post("/images/:action", adminOnly, Images);
 
+
 router.get("/volumes", adminOnly, Volumes);
+router.get("/:host?/volumes", adminOnly, Volumes);
 router.post("/volumes", adminOnly, Volumes);
 router.post("/addVolume", adminOnly, addVolume);
 router.post("/removeVolume", adminOnly, removeVolume);

+ 117 - 5
server.js

@@ -3,9 +3,13 @@ import session from 'express-session';
 import memorystore from 'memorystore';
 import ejs from 'ejs';
 import { router } from './router/index.js';
-import { sequelize } from './database/models.js';
+import { sequelize, ServerSettings } from './database/models.js';
+
 import Docker from 'dockerode';
-export var docker = new Docker();
+
+export var [ docker, docker2, docker3, docker4 ] = [ null, null, null, null ];
+export let [ host_list, host2_list, host3_list, host4_list ] = [ [], [], [], [] ];
+var docker = new Docker();
 
 // Session middleware
 const secure = process.env.HTTPS || false;
@@ -38,12 +42,120 @@ app.use([
 app.listen(PORT, async () => {
     async function init() {// I made sure the console.logs and emojis lined up
         try { await sequelize.authenticate().then(
-            () => { console.log('DB Connection: ✔️') }); }
+            () => { console.log('DB Connection: ') }); }
             catch { console.log('DB Connection: ❌'); }
         try { await sequelize.sync().then(
-            () => { console.log('Synced Models: ✔️') }); }
+            () => { console.log('Synced Models: ') }); }
             catch { console.log('Synced Models: ❌'); } }
         await init().then(() => { 
+            newEvent('host');
             console.log(`Listening on http://localhost:${PORT}`);
     });
-});
+});
+
+// Configure Docker hosts.
+for (let i = 2; i < 5; i++) {
+    try {
+        if (i == 2) { 
+            let config = await ServerSettings.findOne({ where: { key: 'host2' }});
+            if (config.value != 'off' && config.value != '') {
+                let values = config.value.split(',');
+                let port = values[2];
+                let address = values[1];
+                docker2 = new Docker({protocol:'http', host: address, port: port});
+                console.log(`Configured ${host} on ${address}:${port}`);
+                let test = await docker2.listContainers({ all: true });
+                console.log(`${host}: ${test.length} containers`);
+            }
+        } else if (i == 3) {
+            let config = await ServerSettings.findOne({ where: { key: 'host3' }});
+            if (config.value != 'off' && config.value != '') {
+                let values = config.value.split(',');
+                let port = values[2];
+                let address = values[1];
+                docker3 = new Docker({protocol:'http', host: address, port: port});
+                console.log(`Configured ${host} on ${address}:${port}`);
+                let test = await docker3.listContainers({ all: true });
+                console.log(`${host}: ${test.length} containers`);
+            }
+        } else if (i == 4) {
+            let config = await ServerSettings.findOne({ where: { key: 'host4' }});
+            if (config.value != 'off' && config.value != '') {
+                let values = config.value.split(',');
+                let port = values[2];
+                let address = values[1];
+                docker4 = new Docker({protocol:'http', host: address, port: port});
+                console.log(`Configured ${host} on ${address}:${port}`);
+                let test = await docker4.listContainers({ all: true });
+                console.log(`${host}: ${test.length} containers`);
+            }
+        }
+
+    } catch {
+       console.log(`host${i}: Not configured.`);
+    }
+}
+
+
+
+async function updateList(host) {
+    if (host == 'host') {
+        let containers = await docker.listContainers({ all: true });
+        host_list = containers.map(container => ({ containerID: container.Id, containers: container.State }));
+    } else if (host == 'host2') {
+        let containers = await docker2.listContainers({ all: true });
+        host2_list = containers.map(container => ({ containerID: container.Id, containers: container.State }));
+    } else if (host == 'host3') {
+        let containers = await docker3.listContainers({ all: true });
+        host3_list = containers.map(container => ({ containerID: container.Id, containers: container.State }));
+    } else if (host == 'host4') {
+        let containers = await docker4.listContainers({ all: true });
+        host4_list = containers.map(container => ({ containerID: container.Id, containers: container.State }));
+    }
+}
+
+let event = false;
+let skipped_events = 0;
+// Debounce.
+function newEvent(host) {
+    if (!event) {
+        event = true;
+        console.log(`New event from ${host}`);
+        updateList(host);
+        setTimeout(() => {
+            event = false;
+            console.log(`Skipped ${skipped_events} events`);
+            skipped_events = 0;
+        }, 300);
+    } else { skipped_events++; }
+}
+
+docker.getEvents({}, function (err, data) {
+    data.on('data', function () {
+        newEvent('host');
+    });
+}); 
+
+// if (docker2) {
+//     docker2.getEvents({}, function (err, data) {
+//         data.on('data', function () {
+//             newEvent('host2');
+//         });
+//     });
+// }
+
+// if (docker3) {
+//     docker3.getEvents({}, function (err, data) {
+//         data.on('data', function () {
+//             newEvent('host3');
+//         });
+//     });
+// }
+
+// if (docker4) {
+//     docker4.getEvents({}, function (err, data) {
+//         data.on('data', function () {
+//             newEvent('host4');
+//         });
+//     });
+// }

+ 6 - 6
utils/permissions.js

@@ -6,27 +6,27 @@ export const adminOnly = async (req, res, next) => {
 }
 
 export const sessionCheck = async (req, res, next) => {
-    if (req.session.user) { next(); }
+    if (req.session.username) { next(); }
     else { res.redirect('/login'); }
 }
 
 export const permissionCheck = async (req, res, next) => {
     if (req.session.role == 'admin') { next(); return; }
-    let user = req.session.user;
+    let username = req.session.username;
     let action = req.path.split("/")[2];
-    let trigger = req.header('hx-trigger-name');
+    let container_id = req.header('hx-trigger-name');
     const userAction = ['start', 'stop', 'restart', 'pause', 'uninstall', 'upgrade', 'edit', 'logs', 'view'];
     const userPaths = ['card', 'updates', 'hide', 'reset', 'alert'];
     if (userAction.includes(action)) {
-        let permission = await Permission.findOne({ where: { containerName: trigger, user: user }, attributes: [`${action}`] });
+        let permission = await Permission.findOne({ where: { containerID: container_id, userID: req.session.userID }, attributes: [`${action}`] });
         if (permission) { 
             if (permission[action] == true) {
-                console.log(`User ${user} has permission to ${action} ${trigger}`);
+                console.log(`User ${username} has permission to ${action} ${trigger}`);
                 next();
                 return;
             }
             else {
-                console.log(`User ${user} does not have permission to ${action} ${trigger}`);
+                console.log(`User ${username} does not have permission to ${action} ${trigger}`);
             }
         }
     } else if (userPaths.includes(action)) {

+ 1 - 1
views/account.html

@@ -61,7 +61,7 @@
 										<div class="row g-3">
 											<div class="col-md">
 												<div class="form-label">Display Name</div>
-												<input type="text" class="form-control" value="<%= name %>">
+												<input type="text" class="form-control" value="<%= username %>">
 											</div>
 											<div class="col-md">
 												<div class="form-label">First Name</div>

+ 1 - 1
views/modals/permissions.html

@@ -12,7 +12,7 @@
             <div class="row">
                 <div class="col">
                     <form id="reset_permissions">
-                        <input type="hidden" name="container" value="PermissionsContainer">
+                        <input type="hidden" name="containerID" value="ContainerID">
                             <button type="button" class="btn btn-danger" data-bs-dismiss="modal" name="reset_permissions" value="reset_permissions" id="submit" hx-post="/updatePermissions" hx-trigger="click" hx-confirm="Are you sure you want to reset permissions for this container?">Reset</button>
                     </form>
                 </div>

+ 9 - 9
views/partials/containerFull.html

@@ -1,4 +1,4 @@
-  <div class="col-sm-6 col-lg-3 pt-1" hx-post="/dashboard/card" hx-trigger="sse:AppName" hx-swap="outerHTML" name="AppName">
+  <div class="col-sm-6 col-lg-3 pt-1" hx-post="/dashboard/card" hx-trigger="sse:AppName" hx-swap="outerHTML" name="AppName" id="AppID">
     <div class="card">
       <div class="card-body">
         <div class="card-stamp card-stamp-sm">
@@ -8,16 +8,16 @@
           <div class="subheader text-yellow">ExternalPort:InternalPort</div>
             <div class="ms-auto lh-1">
               <div class="card-actions btn-actions">
-                <button class="btn-action" title="Start" data-hx-post="/dashboard/start" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
+                <button class="btn-action" title="Start" data-hx-post="/container/start" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppID" hx-swap="innerHTML">
                   <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
                 </button>
-                <button class="btn-action" title="Stop" data-hx-post="/dashboard/stop" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
+                <button class="btn-action" title="Stop" data-hx-post="/container/stop" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppID" hx-swap="innerHTML">
                   <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
                 </button>
-                <button class="btn-action" title="Pause" data-hx-post="/dashboard/pause" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
+                <button class="btn-action" title="Pause" data-hx-post="/container/pause" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppID" hx-swap="innerHTML">
                   <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
                 </button>
-                <button class="btn-action" title="Restart" data-hx-post="/dashboard/restart" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
+                <button class="btn-action" title="Restart" data-hx-post="/container/restart" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppID" hx-swap="innerHTML">
                   <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>                          
                 </button>
                 <div class="dropdown">
@@ -25,11 +25,11 @@
                     <svg xmlns="http://www.w3.org/2000/svg" class="" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
                   </a>
                   <div class="dropdown-menu dropdown-menu-end">
-                    <button class="dropdown-item text-secondary" name="AppName" id="details" data-hx-post="/dashboard/details" hx-swap="innerHTML" data-hx-trigger="mousedown" data-hx-target="#modals-here" data-bs-toggle="modal" data-bs-target="#modals-here">Details</button>
-                    <button class="dropdown-item text-secondary" name="AppName" id="logs" data-hx-post="/dashboard/logs" hx-swap="innerHTML" hx-trigger="mousedown" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
+                    <button class="dropdown-item text-secondary" name="AppName" id="details" data-hx-post="/container/details" hx-swap="innerHTML" data-hx-trigger="mousedown" data-hx-target="#modals-here" data-bs-toggle="modal" data-bs-target="#modals-here">Details</button>
+                    <button class="dropdown-item text-secondary" name="AppName" id="logs" data-hx-post="/container/logs" hx-swap="innerHTML" hx-trigger="mousedown" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
                     <button class="dropdown-item text-secondary" name="AppName" id="edit">Edit</button>
                     <button class="dropdown-item text-primary" name="AppName" id="update" disabled="">Update</button>
-                    <button class="dropdown-item text-danger" name="AppName" id="uninstall" hx-trigger="mousedown" data-hx-post="/dashboard/uninstall" hx-swap="innerHTML" data-bs-toggle="modal" data-hx-target="#modals-here" data-bs-target="#modals-here">Uninstall</button>
+                    <button class="dropdown-item text-danger" name="AppName" id="uninstall" hx-trigger="mousedown" data-hx-post="/container/uninstall" hx-swap="innerHTML" data-bs-toggle="modal" data-hx-target="#modals-here" data-bs-target="#modals-here">Uninstall</button>
                   </div>
                 </div>
                 <div class="dropdown">
@@ -37,7 +37,7 @@
                     <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /> </svg>
                   </a>
                   <div class="dropdown-menu dropdown-menu-end">
-                    <button class="dropdown-item text-secondary" data-hx-post="/dashboard/hide" data-hx-trigger="mousedown" data-hx-swap="none" name="AppName" id="hide" value="hide">Hide</button>
+                    <button class="dropdown-item text-secondary" data-hx-post="/container/hide" data-hx-trigger="mousedown" data-hx-swap="none" name="AppName" id="AppID" value="hide">Hide</button>
                     <button class="dropdown-item text-secondary" data-hx-post="/dashboard/permissions" name="AppName" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="mousedown" data-bs-toggle="modal" data-bs-target="#modals-here">Permissions</button>
                   </div>
               </div>

+ 2 - 2
views/partials/navbar.html

@@ -195,7 +195,7 @@
           <span class="avatar avatar-sm bg-green-lt"><%= avatar %></span></span>
           <div class="d-none d-xl-block ps-2">
             <div>
-              <%= name %>
+              <%= username %>
             </div>
             <div class="mt-1 small text-muted">
               <%= role %>
@@ -322,7 +322,7 @@
                 <svg xmlns="http://www.w3.org/2000/svg" class="" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
               </a>
               <div class="dropdown-menu dropdown-menu-end">
-                <button class="dropdown-item text-secondary" data-hx-post="/dashboard/reset" data-hx-trigger="mousedown" data-hx-swap="none" name="AppName" id="reset" value="reset">Reset View</button>
+                <button class="dropdown-item text-secondary" data-hx-post="/container/hide" data-hx-trigger="mousedown" data-hx-swap="none" name="reset" id="reset" value="reset">Reset View</button>
               </div>
             </div>
           </div>

+ 3 - 3
views/partials/settings.html

@@ -56,7 +56,7 @@
 						</label>
 					</div>
 					<div class="col-2">
-						<input type="text" class="form-control" name="tag2" placeholder="Tag" data-Tag2>
+						<input type="text" class="form-control" name="tag2" value="Host 2" placeholder="Tag" data-Tag2>
 					</div>
 					<div class="col-4">
 						<input type="text" class="form-control" name="ip2" placeholder="Host IP" data-Ip2>
@@ -80,7 +80,7 @@
 						</label>
 					</div>
 					<div class="col-2">
-						<input type="text" class="form-control" name="tag3" placeholder="Tag" data-Tag3>
+						<input type="text" class="form-control" name="tag3" value="Host 3" placeholder="Tag" data-Tag3>
 					</div>
 					<div class="col-4">
 						<input type="text" class="form-control" name="ip3" placeholder="Host IP" data-Ip3>
@@ -104,7 +104,7 @@
 						</label>
 					</div>
 					<div class="col-2">
-						<input type="text" class="form-control" name="tag4" placeholder="Tag" data-Tag4>
+						<input type="text" class="form-control" name="tag4" value="Host 4" placeholder="Tag" data-Tag4>
 					</div>
 					<div class="col-4">
 						<input type="text" class="form-control" name="ip4" placeholder="Host IP" data-Ip4>

+ 2 - 1
views/partials/user_permissions.html

@@ -27,8 +27,9 @@
                 </div>
               </div>
 
-              <input type="hidden" name="user" value="PermissionsUsername">
+              <input type="hidden" name="userID" value="PermissionsUserID">
               <input type="hidden" name="container" value="PermissionsContainer">
+              <input type="hidden" name="containerID" value="PermissionsID">
 
               <div class="row mb-2">
                 <div class="col-9">

+ 6 - 6
views/register.html

@@ -56,14 +56,14 @@
             <div class="row row-cards">
               <div class="col-sm-6 col-md-6">
                 <div class="mb-2">
-                  <label class="form-label">Name</label>
-                  <input type="text" class="form-control" id="name" name="name">
+                  <label class="form-label">Full Name</label>
+                  <input type="text" class="form-control" name="name" placeholder="John Doe">
                 </div>
               </div>
               <div class="col-sm-6 col-md-6">
                 <div class="mb-2">
-                  <label class="form-label">Username</label>
-                  <input type="text" class="form-control" id="username" name="username">
+                  <label class="form-label">User Name</label>
+                  <input type="text" class="form-control" name="username" placeholder="JDoe">
                 </div>
               </div>
             </div>
@@ -74,13 +74,13 @@
             <div class="mb-2">
               <label class="form-label">Password</label>
               <div class="input-group input-group-flat">
-                <input type="password" class="form-control" id="password" name="password"  autocomplete="off">
+                <input type="password" class="form-control" name="password1"  autocomplete="off">
               </div>
             </div>
             <div class="mb-2">
               <label class="form-label">Confirm Password</label>
               <div class="input-group input-group-flat">
-                <input type="password" class="form-control" id="confirmPassword" name="confirmPassword"  autocomplete="off">
+                <input type="password" class="form-control" name="password2"  autocomplete="off">
               </div>
             </div>