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(`
`);
+ }
+
+ // 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(`Updated `);
- return;
- } else if (trigger_id == 'submit'){
+ // If the trigger is 'submit', return the button
+ if (trigger_id == 'submit'){
res.send(`Update `);
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(`Updated `);
}
\ 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 `
@@ -55,10 +58,19 @@ export function Alert (type, message) {
`;
}
+
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 %>
-
+
-
- <% if(container_list) { %>
- <%- container_list %>
- <% } %>
+
+
+
+
+
+
+
+
@@ -143,226 +139,39 @@
-
-
-
-