diff --git a/CHANGELOG.md b/CHANGELOG.md index fd7789e..555f20b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ * Updated adm-zip. * Updated yaml. * Pushed new docker image with 'latest' tag. -* Updated compose.yaml volume to /app/config. * Fixed container card links. * Moved 'Reset view' button. * New - 'Grid view' and 'List view' button (non-functioning). @@ -18,6 +17,7 @@ * Fixed issue updating view permission. * Fixed issue viewing container logs. * App icons are now determined by service label instead of image name. +* App icons sourced from new repo with 1000+ icons. ## v0.60 (June 9th 2024) - Permissions system and import templates * Converted JS template literals into HTML. diff --git a/controllers/dashboard.js b/controllers/dashboard.js index a2cc4b4..253b168 100644 --- a/controllers/dashboard.js +++ b/controllers/dashboard.js @@ -1,70 +1,248 @@ import { currentLoad, mem, networkStats, fsSize } from 'systeminformation'; -import { containerList, containerInspect } from '../utils/docker.js'; +import { docker, getContainer, containerInspect } from '../utils/docker.js'; import { readFileSync } from 'fs'; -import { User } from '../database/config.js'; -import { Alert, getLanguage, Navbar } from '../utils/system.js'; +import { User, Permission } from '../database/config.js'; +import { Alert, Navbar, Capitalize } from '../utils/system.js'; +import { Op } from 'sequelize'; -export const Dashboard = async function(req,res){ - - let container_list = ''; - - let containers = await containerList(); - for (let container of containers) { - let details = await containerInspect(container.containerID); - let container_card = readFileSync('./views/partials/container_card.html', 'utf8'); - - if (details.name.length > 17) { - details.name = details.name.substring(0, 17) + '...'; - } - - // Capitalize the first letter of the name - details.name = details.name.charAt(0).toUpperCase() + details.name.slice(1); - - - let state = details.state; - let state_color = ''; - - switch (state) { - case 'running': - state_color = 'green'; - break; - case 'exited': - state = 'stopped'; - state_color = 'red'; - break; - case 'paused': - state_color = 'orange'; - break; - case 'installing': - state_color = 'blue'; - break; - } - - container_card = container_card.replace(/AppName/g, details.name); - container_card = container_card.replace(/AppService/g, details.service); - container_card = container_card.replace(/AppState/g, state); - container_card = container_card.replace(/StateColor/g, state_color); - - if (details.external_port == 0 && details.internal_port == 0) { - container_card = container_card.replace(/AppPorts/g, ``); - } else { - container_card = container_card.replace(/AppPorts/g, `${details.external_port}:${details.internal_port}`); - } - - - container_list += container_card; - } +let [ hidden, alert, newCards, stats ] = [ '', '', '', {} ]; +let logString = ''; +// Dashboard +export const Dashboard = async function (req, res) { res.render("dashboard",{ alert: '', username: req.session.username, role: req.session.role, - container_list: container_list, navbar: await Navbar(req), }); } +// Dashboard search +export const submitDashboard = async function (req, res) { + console.log('[SubmitDashboard]'); + console.log(req.body); + res.send('ok'); + return; +} + + +export const CardList = async function (req, res) { + + res.send(newCards); + newCards = ''; + return; +} + + +async function containerInfo (containerID) { + + // get the container info + let info = docker.getContainer(containerID); + let container = await info.inspect(); + + let container_name = container.Name.slice(1); + let container_image = container.Config.Image; + let container_service = container.Config.Labels['com.docker.compose.service']; + + let ports_list = []; + let external = 0; + let internal = 0; + + try { + for (const [key, value] of Object.entries(container.HostConfig.PortBindings)) { + let ports = { + check: 'checked', + external: value[0].HostPort, + internal: key.split('/')[0], + protocol: key.split('/')[1] + } + ports_list.push(ports); + } + } catch {} + + try { external = ports_list[0].external; internal = ports_list[0].internal; } catch { } + + let container_info = { + containerName: container_name, + containerID: containerID, + containerImage: container_image, + containerService: container_service, + containerState: container.State.Status, + external_port: external, + internal_port: internal, + ports: ports_list, + volumes: container.Mounts, + env: container.Config.Env, + labels: container.Config.Labels, + link: 'localhost', + } + + return container_info; +} + + +async function userCards (session) { + session.container_list = []; + // check what containers the user wants hidden + let hidden = await Permission.findAll({ where: {userID: session.userID, hide: true}}, { attributes: ['containerID'] }); + hidden = hidden.map((container) => container.containerID); + // check what containers the user has permission to view + 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'] }); + visable = visable.map((container) => container.containerID); + // get all containers + let containers = await docker.listContainers({ all: true }); + // loop through containers + for (let i = 0; i < containers.length; i++) { + let container_name = containers[i].Names[0].split('/').pop(); + // skip hidden containers + if (hidden.includes(containers[i].Id)) { continue; } + // admin can see all containers that they don't have hidden + if (session.role == 'admin') { session.container_list.push({ containerName: container_name, containerID: containers[i].Id, containerState: containers[i].State }); } + // user can see any containers that they have any permissions for + else if (visable.includes(containers[i].Id)){ session.container_list.push({ containerName: container_name, containerID: containers[i].Id, containerState: containers[i].State }); } + } + // 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) { + let container_list = session.container_list; + let sent_list = session.sent_list; + session.new_cards = []; + session.update_list = []; + // loop through the containers list + container_list.forEach(container => { + let { containerName, containerID, containerState } = container; + let sent = sent_list.find(c => c.containerID === containerID); + if (!sent) { session.new_cards.push(containerID);} + else if (sent.containerState !== containerState) { session.update_list.push(containerID); } + }); + // loop through the sent list to see if any containers have been removed + sent_list.forEach(container => { + let { containerName, containerID, containerState } = container; + let exists = container_list.find(c => c.containerID === containerID); + if (!exists) { session.update_list.push(containerID); } + }); +} + + +// Container actions (start, stop, pause, restart, hide) +export const ContainerAction = async (req, res) => { + + // let trigger_id = req.header('hx-trigger'); + let container_name = req.header('hx-trigger-name'); + let containerID = req.params.containerid; + let action = req.params.action; + + console.log(`Container: ${container_name} ID: ${containerID} Action: ${action}`); + + // Reset the view + if (action == 'reset') { + console.log('Resetting view'); + await Permission.update({ hide: false }, { where: { userID: req.session.userID } }); + res.redirect('/dashboard'); + return; + } + + if (action == 'update') { + await userCards(req.session); + if (!req.session.container_list.find(c => c.containerID === containerID)) { + res.send(''); + return; + } else { + let details = await containerInfo(containerID); + let card = await createCard(details); + res.send(card); + return; + } + } + + // Inspect the container + let info = docker.getContainer(containerID); + let container = await info.inspect(); + let containerState = container.State.Status; + + // Displays container state (starting, stopping, restarting, pausing) + function status (state) { + return(`
+ + ${state} +
`); + } + + // Perform the action + if ((action == 'start') && (containerState == 'exited')) { + info.start(); + res.send(status('starting')); + } else if ((action == 'start') && (containerState == 'paused')) { + info.unpause(); + res.send(status('starting')); + } else if ((action == 'stop') && (containerState != 'exited')) { + info.stop(); + res.send(status('stopping')); + } else if ((action == 'pause') && (containerState == 'paused')) { + info.unpause(); + res.send(status('starting')); + } else if ((action == 'pause') && (containerState == 'running')) { + info.pause(); + res.send(status('pausing')); + } else if (action == 'restart') { + info.restart(); + res.send(status('restarting')); + } else if (action == 'hide') { + let exists = await Permission.findOne({ where: { containerID: containerID, userID: req.session.userID }}); + if (!exists) { const newPermission = await Permission.create({ containerName: container_name, containerID: containerID, username: req.session.username, userID: req.session.userID, hide: true }); } + else { exists.update({ hide: true }); } + res.send('ok'); + } +} + + +async function createCard (details) { + // let shortname = details.name.slice(0, 10) + '...'; + // let trigger = 'data-hx-trigger="load, every 3s"'; + + // Capitalize the container name and shorten it if it's too long + let containerName = Capitalize(details.containerName); + if (containerName.length > 17) { containerName = containerName.substring(0, 17) + '...'; } + + let containerID = details.containerID; + let containerState = details.containerState; + let containerService = details.containerService; + let containerStateColor = ''; + + if (containerState == 'running') { containerStateColor = 'green'; } + else if (containerState == 'exited') { containerStateColor = 'red'; containerState = 'stopped'; } + else if (containerState == 'paused') { containerStateColor = 'orange'; } + else { containerStateColor = 'blue'; } + + let container_card = readFileSync('./views/partials/container_card.html', 'utf8'); + + // let links = await ServerSettings.findOne({ where: {key: 'links'}}); + // if (!links) { links = { value: 'localhost' }; } + + container_card = container_card.replace(/ContainerID/g, containerID); + container_card = container_card.replace(/AltID/g, 'a' + containerID); + container_card = container_card.replace(/AppName/g, containerName); + container_card = container_card.replace(/AppService/g, containerService); + container_card = container_card.replace(/AppState/g, containerState); + container_card = container_card.replace(/StateColor/g, containerStateColor); + + if (details.external_port == 0 && details.internal_port == 0) { + container_card = container_card.replace(/AppPorts/g, ``); + } else { + container_card = container_card.replace(/AppPorts/g, `${details.external_port}:${details.internal_port}`); + } + // card = card.replace(/data-trigger=""/, trigger); + return container_card; +} + + // Server metrics (CPU, RAM, TX, RX, DISK) @@ -96,8 +274,50 @@ export const ServerMetrics = async (req, res) => { } -export const submitDashboard = async function(req,res){ - console.log(req.body); - res.send('ok'); - return; +export const SSE = async (req, res) => { + + // Set the response headers + res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); + + async function eventCheck () { + await userCards(req.session); + await updateDashboard(req.session); + + if (JSON.stringify(req.session.sent_list) === JSON.stringify(req.session.container_list)) { console.log('Event - No Change'); return; } + + console.log('Event - Change Detected'); + + for (let i = 0; i < req.session.new_cards.length; i++) { + let details = await containerInfo(req.session.new_cards[i]); + let card = await createCard(details); + newCards += card; + } + + for (let i = 0; i < req.session.update_list.length; i++) { + res.write(`event: ${req.session.update_list[i]}\n`); + res.write(`data: 'update cards'\n\n`); + } + res.write(`event: update\n`); + res.write(`data: 'update cards'\n\n`); + + req.session.sent_list = req.session.container_list.slice(); + } + + await eventCheck(); + + // Listens for docker events. Only triggers every other event. + docker.getEvents({}, function (err, data) { + let count = 0; + data.on('data', async function () { + count++; + if (count % 2 === 0) { + await eventCheck(); + } + }); + }); + + + req.on('close', () => { + }); + } \ No newline at end of file diff --git a/controllers/images.js b/controllers/images.js index 51bab68..41260a1 100644 --- a/controllers/images.js +++ b/controllers/images.js @@ -1,11 +1,11 @@ import { Alert, getLanguage, Navbar } from '../utils/system.js'; -import { containerList, imageList } from '../utils/docker.js'; +import { imageList } from '../utils/docker.js'; export const Images = async function(req,res){ let container_images = []; - let containers = await containerList(); + let containers = await containerList(req); for (let i = 0; i < containers.length; i++) { container_images.push(containers[i].Image); } diff --git a/controllers/login.js b/controllers/login.js index 0934bac..bcdfc3d 100644 --- a/controllers/login.js +++ b/controllers/login.js @@ -1,14 +1,39 @@ import bcrypt from 'bcrypt'; -import { User, Syslog } from '../database/config.js'; +import { User, Syslog, ServerSettings } from '../database/config.js'; -export const Login = function(req,res){ - if (req.session.userID) { res.redirect("/dashboard"); } - else { res.render("login",{ + +// Login page +export const Login = async function(req,res){ + + if (req.session.userID) { res.redirect("/dashboard"); return; } + + let authentication = await ServerSettings.findOne({ where: { key: 'authentication' }}); + if (!authentication) { await ServerSettings.create({ key: 'authentication', value: 'default' }); } + authentication = await ServerSettings.findOne({ where: { key: 'authentication' }}); + + if (authentication.value == 'localhost' && req.hostname == 'localhost') { + req.session.username = 'Localhost'; + req.session.userID = '00000000-0000-0000-0000-000000000000'; + req.session.role = 'admin'; + res.redirect("/dashboard"); + return; + } else if (authentication.value == 'no_auth') { + req.session.username = 'No Auth'; + req.session.userID = '00000000-0000-0000-0000-000000000000'; + req.session.role = 'admin'; + res.redirect("/dashboard"); + return; + } + + res.render("login",{ "error":"", - }); } + }); } + + +// Submit login export const submitLogin = async function(req,res){ const { password } = req.body; let email = req.body.email.toLowerCase(); @@ -27,9 +52,9 @@ export const submitLogin = async function(req,res){ req.session.role = user.role; res.redirect("/dashboard"); } - } +// Logout export const Logout = function(req,res){ req.session.destroy(() => { res.redirect("/login"); diff --git a/controllers/register.js b/controllers/register.js index 27b6a50..b0b72ce 100644 --- a/controllers/register.js +++ b/controllers/register.js @@ -1,6 +1,6 @@ import bcrypt from "bcrypt"; import { Op } from "sequelize"; -import { User, ServerSettings } from "../database/config.js"; +import { User, ServerSettings, Permission } from "../database/config.js"; export const Register = async function(req,res){ @@ -8,19 +8,21 @@ export const Register = async function(req,res){ // Redirect to dashboard if user is already logged in. if (req.session.username) { res.redirect("/dashboard"); } + + let user_registration = await ServerSettings.findOne({ where: { key: 'user_registration' }}); + let secret_input = ''; - let registration_secret = await ServerSettings.findOne({ where: { key: 'registration' }}).value; // Input field for secret if one has been set. - if (registration_secret) { + if (user_registration) { secret_input = `
- +
`} - // If there are no users, or a registration secret has not been set, display the registration page. - if ((await User.count() == 0) || (registration_secret == '')) { + // If there are no users, or registration has been enabled, display the registration page. + if ((await User.count() == 0) || (user_registration.value == true)) { res.render("register",{ "error": "", "reg_secret": secret_input, @@ -37,7 +39,7 @@ export const submitRegister = async function(req,res){ const { name, username, password, confirm, secret } = req.body; let email = req.body.email.toLowerCase(); - let registration_secret = await ServerSettings.findOne({ where: { key: 'registration' }}).value; + let registration_secret = await ServerSettings.findOne({ where: { key: 'registration_secret' }}).value; let error = ''; if (!name || !username || !email || !password || !confirm) { error = "All fields are required"; } @@ -72,6 +74,7 @@ export const submitRegister = async function(req,res){ let match = await bcrypt.compare(password, user.password); if (match) { console.log(`User ${username} created`); + req.session.username = user.username; req.session.userID = user.userID; req.session.role = user.role; diff --git a/controllers/settings.js b/controllers/settings.js index 33f9742..9ec8d8f 100644 --- a/controllers/settings.js +++ b/controllers/settings.js @@ -3,48 +3,163 @@ import { Alert, getLanguage, Navbar } from '../utils/system.js'; export const Settings = async function(req,res){ - let container_links = await ServerSettings.findOne({ where: {key: 'container_links'}}); + let custom_link = await ServerSettings.findOne({ where: {key: 'custom_link'}}); + let link_url = await ServerSettings.findOne({ where: {key: 'link_url'}}); + let user_registration = await ServerSettings.findOne({ where: {key: 'user_registration'}}); + let registration_secret = await ServerSettings.findOne({ where: {key: 'registration_secret'}}); + + let authentication = await ServerSettings.findOne({ where: {key: 'authentication'}}); + + let custom_link_enabled = ''; + try { if (custom_link.value == true) { custom_link_enabled = 'checked'; } } catch { console.log('Custom Link: No Value Set'); } + + let user_registration_enabled = ''; + try { if (user_registration.value == true) { user_registration_enabled = 'checked'; } } catch { console.log('User Registration: No Value Set'); } + + let link_url_value = ''; + try { link_url_value = link_url.value; } catch { console.log('Link URL: No Value Set'); } + + let registration_secret_value = ''; + try { registration_secret_value = registration_secret.value; } catch { console.log('Registration Secret: No Value Set'); } res.render("settings",{ alert: '', username: req.session.username, role: req.session.role, - user_registration: 'checked', - registration_secret: 'some-long-secret', - container_links: 'checked', - link_url: 'mydomain.com', + user_registration: user_registration_enabled, + registration_secret: registration_secret_value, + custom_link: custom_link_enabled, + link_url: link_url_value, + authentication: authentication.value, navbar: await Navbar(req), }); } -export const submitSettings = async function(req,res){ +export const updateSettings = async function (req, res) { - console.log(req.body); + let { user_registration, registration_secret, custom_link, link_url, authentication } = req.body; + let { host2, tag2, ip2, port2 } = req.body; + let { host3, tag3, ip3, port3 } = req.body; + let { host4, tag4, ip4, port4 } = req.body; let trigger_name = req.header('hx-trigger-name'); let trigger_id = req.header('hx-trigger'); - console.log(`trigger_name: ${trigger_name} - trigger_id: ${trigger_id}`); - - - // [HTMX Triggered] Changes the update button. - if(trigger_id == 'settings'){ - res.send(``); - return; - } else if (trigger_id == 'submit'){ + // If the trigger is 'submit', return the button + if (trigger_id == 'submit'){ res.send(``); return; } - res.render("settings",{ - alert: '', - username: req.session.username, - role: req.session.role, - navbar: await Navbar(req), - }); + // Continues on if the trigger is 'settings + // Custom link + if (custom_link) { + let exists = await ServerSettings.findOne({ where: {key: 'custom_link'}}); + if (exists) { await ServerSettings.update({value: true}, {where: {key: 'custom_link'}}); } + else { await ServerSettings.create({ key: 'custom_link', value: true}); } + + let exists2 = await ServerSettings.findOne({ where: {key: 'link_url'}}); + if (exists2) { await ServerSettings.update({value: link_url}, {where: {key: 'link_url'}}); } + else { await ServerSettings.create({ key: 'link_url', value: link_url}); } + + console.log('Custom link enabled'); + + } else if (!custom_link) { + let exists = await ServerSettings.findOne({ where: {key: 'custom_link'}}); + if (exists) { await ServerSettings.update({value: false}, {where: {key: 'custom_link'}}); } + else { await ServerSettings.create({ key: 'custom_link', value: false}); } + + let exists2 = await ServerSettings.findOne({ where: {key: 'link_url'}}); + if (exists2) { await ServerSettings.update({value: ''}, {where: {key: 'link_url'}}); } + else { await ServerSettings.create({ key: 'link_url', value: ''}); } + + console.log('Custom links off'); + } + + // User registration + if (user_registration) { + let exists = await ServerSettings.findOne({ where: {key: 'user_registration'}}); + if (exists) { const setting = await ServerSettings.update({value: true}, {where: {key: 'user_registration'}}); } + else { const newSetting = await ServerSettings.create({ key: 'user_registration', value: true}); } + + let exists2 = await ServerSettings.findOne({ where: {key: 'registration_secret'}}); + if (exists2) { await ServerSettings.update({value: registration_secret}, {where: {key: 'registration_secret'}}); } + else { await ServerSettings.create({ key: 'registration_secret', value: registration_secret}); } + + console.log('registration on'); + + } else if (!user_registration) { + let exists = await ServerSettings.findOne({ where: {key: 'user_registration'}}); + if (exists) { await ServerSettings.update({value: false}, {where: {key: 'user_registration'}}); } + else { await ServerSettings.create({ key: 'user_registration', value: false}); } + + let exists2 = await ServerSettings.findOne({ where: {key: 'registration_secret'}}); + if (exists2) { await ServerSettings.update({value: ''}, {where: {key: 'registration_secret'}}); } + else { await ServerSettings.create({ key: 'registration_secret', value: ''}); } + + console.log('registration off'); + } + + // Authentication + if (authentication) { + let exists = await ServerSettings.findOne({ where: {key: 'authentication'}}); + if (exists) { await ServerSettings.update({value: authentication}, {where: {key: 'authentication'}}); } + else { await ServerSettings.create({ key: 'authentication', value: authentication}); } + console.log('Authentication on'); + } else if (!authentication) { + let exists = await ServerSettings.findOne({ where: {key: 'authentication'}}); + if (exists) { await ServerSettings.update({value: 'default'}, {where: {key: 'authentication'}}); } + else { await ServerSettings.create({ key: 'authentication', value: 'off'}); } + console.log('Authentication off'); + } + + + + // Host 2 + if (host2) { + let exists = await ServerSettings.findOne({ where: {key: 'host2'}}); + if (exists) { const setting = await ServerSettings.update({value: `${tag2},${ip2},${port2}`}, {where: {key: 'host2'}}); } + else { const newSetting = await ServerSettings.create({ key: 'host2', value: `${tag2},${ip2},${port2}`}); } + console.log('host2 on'); + } else if (!host2) { + let exists = await ServerSettings.findOne({ where: {key: 'host2'}}); + if (exists) { const setting = await ServerSettings.update({value: ''}, {where: {key: 'host2'}}); } + else { const newSetting = await ServerSettings.create({ key: 'host2', value: ''}); } + console.log('host2 off'); + } + + // // Host 3 + if (host3) { + let exists = await ServerSettings.findOne({ where: {key: 'host3'}}); + if (exists) { const setting = await ServerSettings.update({value: `${tag3},${ip3},${port3}`}, {where: {key: 'host3'}}); } + else { const newSetting = await ServerSettings.create({ key: 'host3', value: `${tag3},${ip3},${port3}`}); } + console.log('host3 on'); + } else if (!host3) { + let exists = await ServerSettings.findOne({ where: {key: 'host3'}}); + if (exists) { const setting = await ServerSettings.update({value: ''}, {where: {key: 'host3'}}); } + else { const newSetting = await ServerSettings.create({ key: 'host3', value: ''}); } + console.log('host3 off'); + } + + // Host 4 + if (host4) { + let exists = await ServerSettings.findOne({ where: {key: 'host4'}}); + if (exists) { const setting = await ServerSettings.update({value: `${tag4},${ip4},${port4}`}, {where: {key: 'host4'}}); } + else { const newSetting = await ServerSettings.create({ key: 'host4', value: `${tag4},${ip4},${port4}`}); } + console.log('host4 on'); + } else if (!host4) { + let exists = await ServerSettings.findOne({ where: {key: 'host4'}}); + if (exists) { const setting = await ServerSettings.update({value: ''}, {where: {key: 'host4'}}); } + else { const newSetting = await ServerSettings.create({ key: 'host4', value: ''}); } + console.log('host4 off'); + } + + + console.log('Settings updated'); + res.send(``); } \ No newline at end of file diff --git a/languages/chinese.json b/languages/chinese.json index b6a05f0..4de7b0d 100644 --- a/languages/chinese.json +++ b/languages/chinese.json @@ -15,19 +15,24 @@ "admin": "", "user": "", "Start": "", - "Starting": "", - "Running": "", "Stop": "", - "Stopping": "", - "Stopped": "", "Pause": "", - "Pausing": "", - "Paused": "", "Restart": "", + "Starting": "", + "Stopping": "", + "Pausing": "", "Restarting": "", + "Running": "", + "Stopped": "", + "Paused": "", "Details": "", "Logs": "", "Edit": "", "Update": "", - "Uninstall": "" + "Uninstall": "", + "Hide": "", + "Reset View": "", + "Permissions": "", + "Sponsors": "", + "Credits": "" } \ No newline at end of file diff --git a/package.json b/package.json index fa32c47..819de73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dweebui", - "version": "0.70.402", + "version": "0.70.417", "main": "server.js", "type": "module", "scripts": { @@ -10,7 +10,7 @@ "keywords": [], "author": "lllllllillllllillll", "license": "MIT", - "description": "DweebUI is a WebUI for managing your containers. Simple setup, a dynamically updating dashboard, and a multi-user permission system.", + "description": "DweebUI is a WebUI for managing your containers. https://dweebui.com", "dependencies": { "bcrypt": "^5.1.1", "connect-session-sequelize": "^7.1.7", diff --git a/public/css/dweebui.css b/public/css/dweebui.css index 6a80efc..22db8f6 100644 --- a/public/css/dweebui.css +++ b/public/css/dweebui.css @@ -1,156 +1,163 @@ +@import url('/fonts/inter.css'); +:root { + --tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif; +} +body { + font-feature-settings: "cv03", "cv04", "cv11"; +} .meter { - box-sizing: content-box; - height: 15px; - margin-left: auto; - margin-right: auto; - position: relative; - background: #a7a7a752; - border-radius: 25px; - padding: 3px; - box-shadow: inset 0 -1px 1px rgba(255, 255, 255, 0.3); - } + box-sizing: content-box; + height: 15px; + margin-left: auto; + margin-right: auto; + position: relative; + background: #a7a7a752; + border-radius: 25px; + padding: 3px; + box-shadow: inset 0 -1px 1px rgba(255, 255, 255, 0.3); +} - .meter > span { - display: block; - height: 100%; - border-top-right-radius: 20px; - border-bottom-right-radius: 20px; - border-top-left-radius: 20px; - border-bottom-left-radius: 20px; - background-color: rgb(43, 194, 83); - background-image: linear-gradient( - center bottom, - rgb(43, 194, 83) 37%, - rgb(84, 240, 84) 69% - ); - box-shadow: inset 0 2px 9px rgba(255, 255, 255, 0.3), - inset 0 -2px 6px rgba(0, 0, 0, 0.4); - position: relative; - overflow: hidden; - } +.meter > span { + display: block; + height: 100%; + border-top-right-radius: 20px; + border-bottom-right-radius: 20px; + border-top-left-radius: 20px; + border-bottom-left-radius: 20px; + background-color: rgb(43, 194, 83); + background-image: linear-gradient( + center bottom, + rgb(43, 194, 83) 37%, + rgb(84, 240, 84) 69% + ); + box-shadow: inset 0 2px 9px rgba(255, 255, 255, 0.3), + inset 0 -2px 6px rgba(0, 0, 0, 0.4); + position: relative; + overflow: hidden; +} - .meter > span:after, - .animate > span > span { - content: ""; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - background-image: linear-gradient( - -45deg, - rgba(255, 255, 255, 0.2) 25%, - transparent 25%, - transparent 50%, - rgba(255, 255, 255, 0.2) 50%, - rgba(255, 255, 255, 0.2) 75%, - transparent 75%, - transparent - ); - z-index: 1; - background-size: 50px 50px; - animation: move 2s linear infinite; - border-top-right-radius: 8px; - border-bottom-right-radius: 8px; - border-top-left-radius: 20px; - border-bottom-left-radius: 20px; - overflow: hidden; - } - - .animate > span:after { - display: none; - } - - @keyframes move { - 0% { - background-position: 0 0; - } - 100% { - background-position: 50px 50px; - } - } - - .orange > span { - background-image: linear-gradient(#f1a165, #f36d0a); - } - - .red > span { - background-image: linear-gradient(#f0a3a3, #f42323); - } +.meter > span:after, +.animate > span > span { + content: ""; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background-image: linear-gradient( + -45deg, + rgba(255, 255, 255, 0.2) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.2) 50%, + rgba(255, 255, 255, 0.2) 75%, + transparent 75%, + transparent + ); + z-index: 1; + background-size: 50px 50px; + animation: move 2s linear infinite; + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; + border-top-left-radius: 20px; + border-bottom-left-radius: 20px; + overflow: hidden; +} - .blue > span { - background-image: linear-gradient(#2478f5, #22017e); - } +.animate > span:after { + display: none; +} - .purple > span { - background-image: linear-gradient(#bd14d3, #670370); +@keyframes move { + 0% { + background-position: 0 0; } - - .nostripes > span > span, - .nostripes > span::after { - background-image: none; + 100% { + background-position: 50px 50px; } +} + +.orange > span { + background-image: linear-gradient(#f1a165, #f36d0a); +} + +.red > span { + background-image: linear-gradient(#f0a3a3, #f42323); +} + +.blue > span { + background-image: linear-gradient(#2478f5, #22017e); +} + +.purple > span { + background-image: linear-gradient(#bd14d3, #670370); +} + +.nostripes > span > span, +.nostripes > span::after { + background-image: none; +} - .container-stamp { - --tblr-stamp-size: 8rem; - position: absolute; - bottom: 0; - left: 0; - width: calc(var(--tblr-stamp-size) * 1); - height: calc(var(--tblr-stamp-size) * 1); - max-height: 100%; - border-top-right-radius: 4px; - opacity: 0.2; - overflow: hidden; - pointer-events: none; - } - - .container-action { - padding: 0; - border: 0; - color: var(--tblr-secondary); - display: inline-flex; - width: 1.5rem; - height: 1.5rem; - align-items: center; - justify-content: center; - border-radius: var(--tblr-border-radius); - background: transparent; - } - .container-action:after { - content: none; - } - .container-action:focus { - outline: none; - box-shadow: none; - } - .container-action:hover, .container-action.show { - color: var(--tblr-body-color); - background: var(--tblr-active-bg); - } - .container-action.show { - color: var(--tblr-primary); - } - .container-action .icon { - margin: 0; - width: 1.25rem; - height: 1.25rem; - font-size: 1.25rem; - stroke-width: 1; - } - - .container-actions { - display: flex; - } +.container-stamp { + --tblr-stamp-size: 8rem; + position: absolute; + bottom: 0; + left: 0; + width: calc(var(--tblr-stamp-size) * 1); + height: calc(var(--tblr-stamp-size) * 1); + max-height: 100%; + border-top-right-radius: 4px; + opacity: 0.2; + overflow: hidden; + pointer-events: none; +} - .modal-content { - border: 1px solid grey; - } - - .accordion-item { - border: 1px solid grey; - } +.container-action { + padding: 0; + border: 0; + color: var(--tblr-secondary); + display: inline-flex; + width: 1.5rem; + height: 1.5rem; + align-items: center; + justify-content: center; + border-radius: var(--tblr-border-radius); + background: transparent; +} +.container-action:after { + content: none; +} +.container-action:focus { + outline: none; + box-shadow: none; +} +.container-action:hover, .container-action.show { + color: var(--tblr-body-color); + background: var(--tblr-active-bg); +} +.container-action.show { + color: var(--tblr-primary); +} +.container-action .icon { + margin: 0; + width: 1.25rem; + height: 1.25rem; + font-size: 1.25rem; + stroke-width: 1; +} + +.container-actions { + display: flex; +} + +.modal-content { + border: 1px solid grey; +} + +.accordion-user { + border: 1px solid grey; +} diff --git a/public/js/dweebui.js b/public/js/dweebui.js index a8743aa..8eda474 100644 --- a/public/js/dweebui.js +++ b/public/js/dweebui.js @@ -24,4 +24,20 @@ function toggleTheme(button) { document.body.removeAttribute("data-bs-theme"); localStorage.setItem(themeStorageKey, 'light'); } -} \ No newline at end of file +} + + + +function selectAll(group) { + + let checkboxes = document.getElementsByName(group); + if (checkboxes[0].checked == true) { + for (var i = 0; i < checkboxes.length; i++) { + checkboxes[i].checked = true; + } + } else { + for (var i = 0; i < checkboxes.length; i++) { + checkboxes[i].checked = false; + } + } +} diff --git a/router.js b/router.js index 9ee0f39..65d2b3f 100644 --- a/router.js +++ b/router.js @@ -3,19 +3,17 @@ export const router = express.Router(); import { Login, submitLogin, Logout } from './controllers/login.js'; import { Register, submitRegister } from './controllers/register.js'; -import { Dashboard, submitDashboard, ServerMetrics } from './controllers/dashboard.js'; -import { Settings, submitSettings } from './controllers/settings.js'; +import { Dashboard, submitDashboard, ContainerAction, ServerMetrics, CardList, SSE } from './controllers/dashboard.js'; +import { Settings, updateSettings } from './controllers/settings.js'; import { Images, submitImages } from './controllers/images.js'; import { Volumes, submitVolumes } from './controllers/volumes.js'; import { Networks, submitNetworks } from './controllers/networks.js'; import { Users, submitUsers } from './controllers/users.js'; import { Apps, submitApps } from './controllers/apps.js'; import { Account } from './controllers/account.js'; -import { containerAction } from './utils/docker.js'; import { Preferences, submitPreferences } from './controllers/preferences.js'; - -import { sessionCheck, adminOnly, permissionCheck } from './utils/permissions.js'; +import { sessionCheck, adminOnly, permissionCheck, permissionModal } from './utils/permissions.js'; router.get('/login', Login); router.post('/login', submitLogin); @@ -25,8 +23,13 @@ router.post('/register', submitRegister); router.get("/:host?/dashboard", sessionCheck, Dashboard); router.get("/server_metrics", sessionCheck, ServerMetrics); +router.get("/permission_modal", adminOnly, permissionModal); -router.post("/:host?/container/:action", permissionCheck, containerAction); + +router.get("/sse", sessionCheck, SSE); +router.get("/card_list", sessionCheck, CardList); + +router.post("/:host?/container/:action/:containerid?", permissionCheck, ContainerAction); router.get('/images', adminOnly, Images); router.post('/images', adminOnly, submitImages); @@ -38,7 +41,7 @@ router.get('/networks', adminOnly, Networks); router.post('/networks', adminOnly, submitNetworks); router.get('/settings', adminOnly, Settings); -router.post('/settings', adminOnly, submitSettings); +router.post('/settings', adminOnly, updateSettings); router.get("/apps/:page?/:template?", adminOnly, Apps); router.post('/apps', adminOnly, submitApps); diff --git a/utils/docker.js b/utils/docker.js index 824e06b..8fea2bc 100644 --- a/utils/docker.js +++ b/utils/docker.js @@ -1,24 +1,17 @@ import Docker from 'dockerode'; -import { Permission } from '../database/config.js'; export var docker = new Docker(); -export async function containerList() { - let containers = await docker.listContainers({ all: true }); - containers = containers.map(container => ({ - containerName: container.Names[0].split('/').pop(), - containerID: container.Id, - containerState: container.State, - containerImage: container.Image, - })); - return containers; -} export async function imageList() { let images = await docker.listImages({ all: true }); return images; } +export async function getContainer(containerID) { + let container = docker.getContainer(containerID); + return container; +} export async function containerInspect (containerID) { // get the container info @@ -64,72 +57,4 @@ export async function containerInspect (containerID) { link: 'localhost', } return details; -} - - -export const containerAction = async (req, res) => { - - let container_name = req.header('hx-trigger-name'); - let container_id = req.header('hx-trigger'); - let action = req.params.action; - - console.log(`Container: ${container_name} ID: ${container_id} Action: ${action}`); - - // Reset the view - 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}`); - // Displays container state (starting, stopping, restarting, pausing) - function status (state) { - return(` - ${state} - `); - } - // Perform the action - 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); - res.send("ok"); - } -} - - - - -// Listens for docker events -docker.getEvents({}, function (err, data) { - data.on('data', function () { - console.log('Docker event'); - }); -}); \ No newline at end of file +} \ No newline at end of file diff --git a/utils/permissions.js b/utils/permissions.js index 7347549..1434349 100644 --- a/utils/permissions.js +++ b/utils/permissions.js @@ -1,4 +1,5 @@ import { Permission } from "../database/config.js"; +import { readFileSync } from 'fs'; export const adminOnly = async (req, res, next) => { if (req.session.role == 'admin') { next(); } @@ -12,5 +13,45 @@ export const sessionCheck = async (req, res, next) => { export const permissionCheck = async (req, res, next) => { + next(); +} + + + +export const permissionModal = async (req, res) => { + + // let title = name.charAt(0).toUpperCase() + name.slice(1); + // let permissions_list = ''; + let permissions_modal = readFileSync('./views/partials/permissions.html', 'utf8'); + // permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title); + // permissions_modal = permissions_modal.replace(/PermissionsContainer/g, name); + // let users = await User.findAll({ attributes: ['username', 'UUID']}); + // for (let i = 0; i < users.length; i++) { + // 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}}); + // 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'); } + // if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); } + // if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); } + // if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); } + // if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); } + // if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); } + // if (permissions.view == true) { user_permissions = user_permissions.replace(/data-ViewCheck/g, 'checked'); } + // user_permissions = user_permissions.replace(/EntryNumber/g, i); + // user_permissions = user_permissions.replace(/EntryNumber/g, i); + // user_permissions = user_permissions.replace(/EntryNumber/g, i); + // user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username); + // user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username); + // user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username); + // user_permissions = user_permissions.replace(/PermissionsContainer/g, name); + // user_permissions = user_permissions.replace(/PermissionsContainer/g, name); + // user_permissions = user_permissions.replace(/PermissionsContainer/g, name); + // permissions_list += user_permissions; + // } + // permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list); + res.send(permissions_modal); } \ No newline at end of file diff --git a/utils/system.js b/utils/system.js index 8c3fee3..79b001a 100644 --- a/utils/system.js +++ b/utils/system.js @@ -2,17 +2,20 @@ import { User } from '../database/config.js'; import { readFileSync } from 'fs'; + + +// Navbar export async function Navbar (req) { let username = req.session.username; - let language = await getLanguage(req); - let user = await User.findOne({ where: { userID: req.session.userID }}); - let preferences = JSON.parse(user.preferences); - if (preferences.hide_profile == true) { - username = 'Anonymous'; + // Check if the user wants to hide their profile name. + if (req.session.userID != '00000000-0000-0000-0000-000000000000') { + let user = await User.findOne({ where: { userID: req.session.userID }}); + let preferences = JSON.parse(user.preferences); + if (preferences.hide_profile == true) { username = 'Anon'; } } let navbar = readFileSync('./views/partials/navbar.html', 'utf8'); @@ -39,7 +42,7 @@ export async function Navbar (req) { } } - +// Header Alert export function Alert (type, message) { return ` `; } + export async function getLanguage (req) { - let user = await User.findOne({ where: { userID: req.session.userID }}); - let preferences = JSON.parse(user.preferences); - return preferences.language; + + // No userID if authentication is disabled. + if (req.session.userID == '00000000-0000-0000-0000-000000000000') { + let user = await User.findOne({ where: { role: 'admin' }}); + let preferences = JSON.parse(user.preferences); + return preferences.language; + } else { + let user = await User.findOne({ where: { userID: req.session.userID }}); + let preferences = JSON.parse(user.preferences); + return preferences.language; + } } export function Capitalize (string) { diff --git a/views/account.html b/views/account.html index e181fc8..cce03cf 100644 --- a/views/account.html +++ b/views/account.html @@ -6,18 +6,9 @@ Account - DweebUI - - + diff --git a/views/apps.html b/views/apps.html index 0ad73db..dbc15c8 100644 --- a/views/apps.html +++ b/views/apps.html @@ -1,4 +1,5 @@ + @@ -8,18 +9,7 @@ - - -
diff --git a/views/dashboard.html b/views/dashboard.html index 0a5b850..ac214b7 100644 --- a/views/dashboard.html +++ b/views/dashboard.html @@ -6,21 +6,9 @@ Dashboard - DweebUI. - - - - @@ -29,12 +17,10 @@ <%- navbar %> -
+
- -
@@ -73,10 +59,10 @@
- +
- +
@@ -129,10 +115,20 @@
- - <% if(container_list) { %> - <%- container_list %> - <% } %> + + +
+
+
+
+ + +
+
+
+
+ +
@@ -143,226 +139,39 @@
- - - -