diff --git a/README.md b/README.md index 0370499..2beb273 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
@@ -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
diff --git a/compose.yaml b/compose.yaml
index 834aa25..573e79b 100644
--- a/compose.yaml
+++ b/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
diff --git a/controllers/account.js b/controllers/account.js
index be9f141..794bdfd 100644
--- a/controllers/account.js
+++ b/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: '',
diff --git a/controllers/apps.js b/controllers/apps.js
index 9ed2ca9..ec15bcc 100644
--- a/controllers/apps.js
+++ b/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,
diff --git a/controllers/dashboard.js b/controllers/dashboard.js
index 25b838c..477f1d7 100644
--- a/controllers/dashboard.js
+++ b/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 = ``;
- }
-
- let host3 = await hostInfo('host3');
- if (host3) {
- link3 = ``;
- }
+ // let host2 = await hostInfo('host2');
+ // let host3 = await hostInfo('host3');
+ // let host4 = await hostInfo('host4');
- let host4 = await hostInfo('host4');
- if (host4) {
- link4 = ``;
- }
-
- if (host2 || host3 || host4) {
- link1 = `
+ if (docker2 || docker3 || docker4) {
+ link1 = `
Host 1
`;
+ link5 = `
+ All
+ `;
+ }
+ if (docker2) { link2 = `
+ Host2
+ `;
+ }
+ if (docker3) { link3 = `
+ Host3
+ `;
+ }
+ if (docker4) { link4 = `
+ Host4
+ `;
}
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(`
+ ${state}
+ `);
+ }
+
+ 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 = ``;
- } catch {
- console.log(`Error connecting to ${name}`);
- link = ``;
- }
- 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 = ``;
+ // } catch {
+ // console.log(`Error connecting to ${name}`);
+ // link = ``;
+ // }
+ // 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(`
- ${state}
- `);
- }
-
- // 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') {
diff --git a/controllers/images.js b/controllers/images.js
index 4f770bf..498b3cd 100644
--- a/controllers/images.js
+++ b/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: '',
diff --git a/controllers/login.js b/controllers/login.js
index 8a82d30..3d5bf32 100644
--- a/controllers/login.js
+++ b/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 submitLogin = async function(req,res){
-
- if (no_auth && req.hostname == 'localhost') {
- req.session.user = 'Localhost';
- req.session.UUID = '';
- req.session.role = 'admin';
- res.redirect("/dashboard");
- return;
- }
-
- let { email, password } = req.body;
- email = email.toLowerCase();
-
- 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);
- res.render("login",{
- "error":"Please fill in all the fields.",
- });
- }
-}
-
export const Logout = function(req,res){
req.session.destroy(() => {
res.redirect("/login");
});
-}
\ No newline at end of file
+}
+
+
+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.username = 'Localhost';
+ req.session.userID = '';
+ req.session.role = 'admin';
+ res.redirect("/dashboard");
+ return;
+ }
+
+ // Check that all fields are filled out.
+ if (!email || !password) {
+ res.render("login",{
+ "error":"Please fill in all fields.",
+ });
+ return;
+ }
+
+ // Check that the user exists.
+ let user = await User.findOne({ where: { email: email }});
+ if (!user) {
+ res.render("login",{
+ "error":"Invalid credentials.",
+ });
+ return;
+ }
+
+ // Check that the password is correct.
+ let password_check = await bcrypt.compare( password, user.password);
+
+ // 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");
+}
+
+
diff --git a/controllers/networks.js b/controllers/networks.js
index be9142b..01a4f3e 100644
--- a/controllers/networks.js
+++ b/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 += ``
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: '',
diff --git a/controllers/register.js b/controllers/register.js
index 27286b4..0c9624b 100644
--- a/controllers/register.js
+++ b/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 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.",
- });
- }
-
- } else {
- // return an error.
- res.render("register",{
- "error":"User with that email already exists.",
- });
- }
- } else {
- // Redirect to the signup page.
+ // Check that all fields are filled out correctly.
+ if ((!name || !username || !email || !password1 || !password2) || (password1 != password2)) {
res.render("register",{
- "error":"Please fill in all the fields.",
+ "error":"Missing field or password mismatch.",
+ });
+ return;
+ }
+
+ // 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;
+ }
+
+ // 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 {
+ // 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":"User not created",
});
}
}
\ No newline at end of file
diff --git a/controllers/settings.js b/controllers/settings.js
index b09e31a..31d389c 100644
--- a/controllers/settings.js
+++ b/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: '',
diff --git a/controllers/supporters.js b/controllers/supporters.js
index 9b04bbc..c210ef4 100644
--- a/controllers/supporters.js
+++ b/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: '',
diff --git a/controllers/syslogs.js b/controllers/syslogs.js
index bd8d89d..9548030 100644
--- a/controllers/syslogs.js
+++ b/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: '',
diff --git a/controllers/users.js b/controllers/users.js
index a83de1c..a2e268e 100644
--- a/controllers/users.js
+++ b/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: '',
diff --git a/controllers/volumes.js b/controllers/volumes.js
index ac27ff8..6e4164a 100644
--- a/controllers/volumes.js
+++ b/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 = `
@@ -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]);
-// }
-// });
+}
\ No newline at end of file
diff --git a/database/models.js b/database/models.js
index 925d2f2..42cc06d 100644
--- a/database/models.js
+++ b/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
},
diff --git a/router/index.js b/router/index.js
index 141e4a8..cdc0f2a 100644
--- a/router/index.js
+++ b/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);
diff --git a/server.js b/server.js
index cf69ad8..170595c 100644
--- a/server.js
+++ b/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}`);
});
-});
\ No newline at end of file
+});
+
+// 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');
+// });
+// });
+// }
diff --git a/utils/permissions.js b/utils/permissions.js
index 48adfc9..7af07bc 100644
--- a/utils/permissions.js
+++ b/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)) {
diff --git a/views/account.html b/views/account.html
index 1828a0d..8b25117 100644
--- a/views/account.html
+++ b/views/account.html
@@ -61,7 +61,7 @@