diff --git a/README.md b/README.md index 0370499..2beb273 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

-

DweebUI Beta v0.70 ( :fire: Experimental :fire: )

+

DweebUI v0.70 ( :fire: Experimental :fire: )

Free and Open-Source WebUI For Managing Your Containers.

@@ -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 @@

Display Name
- +
First Name
diff --git a/views/modals/permissions.html b/views/modals/permissions.html index ee46a3a..ef95c41 100644 --- a/views/modals/permissions.html +++ b/views/modals/permissions.html @@ -12,7 +12,7 @@
- +
diff --git a/views/partials/containerFull.html b/views/partials/containerFull.html index 2b13436..2330fee 100644 --- a/views/partials/containerFull.html +++ b/views/partials/containerFull.html @@ -1,4 +1,4 @@ -
+
@@ -8,16 +8,16 @@
ExternalPort:InternalPort
- - - - diff --git a/views/partials/navbar.html b/views/partials/navbar.html index bbef0c6..430b29d 100644 --- a/views/partials/navbar.html +++ b/views/partials/navbar.html @@ -195,7 +195,7 @@ <%= avatar %>
- <%= name %> + <%= username %>
<%= role %> @@ -322,7 +322,7 @@
diff --git a/views/partials/settings.html b/views/partials/settings.html index 63cbb0d..cedf622 100644 --- a/views/partials/settings.html +++ b/views/partials/settings.html @@ -56,7 +56,7 @@
- +
@@ -80,7 +80,7 @@
- +
@@ -104,7 +104,7 @@
- +
diff --git a/views/partials/user_permissions.html b/views/partials/user_permissions.html index f5e5c91..397e5f4 100644 --- a/views/partials/user_permissions.html +++ b/views/partials/user_permissions.html @@ -27,8 +27,9 @@
- + +
diff --git a/views/register.html b/views/register.html index 357bfef..0478616 100644 --- a/views/register.html +++ b/views/register.html @@ -56,14 +56,14 @@
- - + +
- - + +
@@ -74,13 +74,13 @@
- +
- +